From b8e1714c0f70f0d053ea6d33f98fd3ece0bdcead Mon Sep 17 00:00:00 2001 From: miav Date: Tue, 4 Apr 2017 21:16:32 +0200 Subject: [PATCH 01/10] Parcelable to ParseACL --- Parse/src/main/java/com/parse/ParseACL.java | 60 ++++++++++++++++++- .../src/main/java/com/parse/ParseObject.java | 18 +++++- .../src/test/java/com/parse/ParseACLTest.java | 36 +++++++++++ 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseACL.java b/Parse/src/main/java/com/parse/ParseACL.java index c7381d563..b9ceffe52 100644 --- a/Parse/src/main/java/com/parse/ParseACL.java +++ b/Parse/src/main/java/com/parse/ParseACL.java @@ -8,12 +8,16 @@ */ package com.parse; +import android.os.Parcel; +import android.os.Parcelable; + import org.json.JSONException; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; +import java.util.Set; /** * A {@code ParseACL} is used to control which users can access or modify a particular object. Each @@ -22,7 +26,7 @@ * permissions to "the public" so that, for example, any user could read a particular object but * only a particular set of users could write to that object. */ -public class ParseACL { +public class ParseACL implements Parcelable { private static final String PUBLIC_KEY = "*"; private final static String UNRESOLVED_KEY = "*unresolved"; private static final String KEY_ROLE_PREFIX = "role:"; @@ -61,6 +65,11 @@ private static class Permissions { return json; } + /* package */ void toParcel(Parcel parcel) { + parcel.writeByte(readPermission ? (byte) 1 : 0); + parcel.writeByte(writePermission ? (byte) 1 : 0); + } + /* package */ boolean getReadPermission() { return readPermission; } @@ -74,6 +83,10 @@ private static class Permissions { boolean write = object.optBoolean(WRITE_PERMISSION, false); return new Permissions(read, write); } + + /* package */ static Permissions createPermissionsFromParcel(Parcel parcel) { + return new Permissions(parcel.readByte() == 1, parcel.readByte() == 1); + } } private static ParseDefaultACLController getDefaultACLController() { @@ -536,4 +549,49 @@ public void done(ParseObject object, ParseException e) { /* package for tests */ Map getPermissionsById() { return permissionsById; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte(shared ? (byte) 1 : 0); + dest.writeInt(permissionsById.size()); + Set keys = permissionsById.keySet(); + for (String key : keys) { + dest.writeString(key); + Permissions permissions = permissionsById.get(key); + permissions.toParcel(dest); + } + dest.writeByte(unresolvedUser != null ? (byte) 1 : 0); + if (unresolvedUser != null) dest.writeParcelable(unresolvedUser, 0); + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseACL createFromParcel(Parcel source) { + return new ParseACL(source); + } + + @Override + public ParseACL[] newArray(int size) { + return new ParseACL[size]; + } + }; + + /* package */ ParseACL(Parcel source) { + shared = source.readByte() == 1; + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + Permissions permissions = Permissions.createPermissionsFromParcel(source); + permissionsById.put(key, permissions); + } + if (source.readByte() == 1) { + unresolvedUser = source.readParcelable(ParseUser.class.getClassLoader()); + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); + } + } } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 8f68fbbf3..4193f2d1f 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -8,6 +8,9 @@ */ package com.parse; +import android.os.Parcel; +import android.os.Parcelable; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -46,7 +49,7 @@ * The basic workflow for accessing existing data is to use a {@link ParseQuery} to specify which * existing data to retrieve. */ -public class ParseObject { +public class ParseObject implements Parcelable { private static final String AUTO_CLASS_NAME = "_Automatic"; /* package */ static final String VERSION_NAME = "1.15.2-SNAPSHOT"; @@ -4171,6 +4174,19 @@ public Task unpinInBackground() { public void unpin() throws ParseException { ParseTaskUtils.wait(unpinInBackground()); } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + + } + + } // [1] Normally we should only construct the command from state when it's our turn in the diff --git a/Parse/src/test/java/com/parse/ParseACLTest.java b/Parse/src/test/java/com/parse/ParseACLTest.java index 5b66104dd..dae17c293 100644 --- a/Parse/src/test/java/com/parse/ParseACLTest.java +++ b/Parse/src/test/java/com/parse/ParseACLTest.java @@ -8,14 +8,19 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import java.util.HashMap; @@ -35,6 +40,8 @@ import static org.mockito.Mockito.when; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) public class ParseACLTest { private final static String UNRESOLVED_KEY = "*unresolved"; @@ -162,6 +169,35 @@ public void testToJson() throws Exception { //endregion + //region parcelable + + @Test + public void testParcelable() throws Exception { + ParseACL acl = new ParseACL(); + acl.setReadAccess("userId", true); + ParseUser user = new ParseUser(); + user.setObjectId("userId2"); + acl.setReadAccess(user, true); + acl.setRoleWriteAccess("role", true); + acl.setShared(true); + + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + acl = ParseACL.CREATOR.createFromParcel(parcel); + + assertTrue(acl.getReadAccess("userId")); + assertTrue(acl.getReadAccess(user)); + assertTrue(acl.getRoleWriteAccess("role")); + assertTrue(acl.isShared()); + assertFalse(acl.getPublicReadAccess()); + assertFalse(acl.getPublicWriteAccess()); + } + + // TODO testParcelableWithUnresolvedUser once we have decent User parceling. + + //endregion + //region testCreateACLFromJSONObject @Test From 5912b05e23920044bc2780f36c1bb5974f7bfbe1 Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 00:22:50 +0200 Subject: [PATCH 02/10] Parcelable to ParseObject and subclasses --- .../src/main/java/com/parse/ParseObject.java | 114 +++++++++++++++ .../com/parse/ParseParcelableDecoder.java | 89 +++++++++++ .../com/parse/ParseParcelableEncoder.java | 138 ++++++++++++++++++ Parse/src/main/java/com/parse/ParseUser.java | 32 ++++ .../java/com/parse/ParseObjectStateTest.java | 2 + .../test/java/com/parse/ParseObjectTest.java | 60 ++++++++ .../test/java/com/parse/ParseUserTest.java | 2 + 7 files changed, 437 insertions(+) create mode 100644 Parse/src/main/java/com/parse/ParseParcelableDecoder.java create mode 100644 Parse/src/main/java/com/parse/ParseParcelableEncoder.java diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 4193f2d1f..b1d817ac9 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -8,6 +8,7 @@ */ package com.parse; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; @@ -97,6 +98,14 @@ public static Init newBuilder(String className) { return new Builder(className); } + /* package */ static State createFromParcel(Parcel source) { + String className = source.readString(); + if ("_User".equals(className)) { + return new ParseUser.State(source, className); + } + return new State(source, className); + } + /** package */ static abstract class Init { private final String className; @@ -265,6 +274,27 @@ public State build() { availableKeys = new HashSet<>(builder.availableKeys); } + /* package */ State(Parcel parcel, String clazz) { + ParseParcelableDecoder decoder = ParseParcelableDecoder.get(); + className = clazz; + objectId = parcel.readByte() == 1 ? parcel.readString() : null; + createdAt = parcel.readLong(); + long updated = parcel.readLong(); + updatedAt = updated > 0 ? updated : createdAt; + int size = parcel.readInt(); + HashMap map = new HashMap<>(); + for (int i = 0; i < size; i++) { + String key = parcel.readString(); + Object obj = decoder.decode(parcel); + map.put(key, obj); + } + serverData = Collections.unmodifiableMap(map); + isComplete = parcel.readByte() == 1; + List available = new ArrayList<>(); + parcel.readStringList(available); + availableKeys = new HashSet<>(available); + } + @SuppressWarnings("unchecked") public > T newBuilder() { return (T) new Builder(this); @@ -307,6 +337,25 @@ public Set availableKeys() { return availableKeys; } + protected void writeToParcel(Parcel dest) { + ParseParcelableEncoder encoder = ParseParcelableEncoder.get(); + dest.writeString(className); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) { + dest.writeString(objectId); + } + dest.writeLong(createdAt); + dest.writeLong(updatedAt); + dest.writeInt(serverData.size()); + Set keys = serverData.keySet(); + for (String key : keys) { + dest.writeString(key); + encoder.encode(serverData.get(key), dest); + } + dest.writeByte(isComplete ? (byte) 1 : 0); + dest.writeStringList(new ArrayList<>(availableKeys)); + } + @Override public String toString() { return String.format(Locale.US, "%s@%s[" + @@ -4183,9 +4232,74 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { + // TODO operationSetQueue? + // TODO isDeletingEventually? + // TODO warn if it has ongoing tasks + ParseParcelableEncoder encoder = ParseParcelableEncoder.get(); + synchronized (mutex) { + state.writeToParcel(dest); + dest.writeByte(localId != null ? (byte) 1 : 0); + if (localId != null) dest.writeString(localId); + dest.writeByte(isDeleted ? (byte) 1 : 0); + dest.writeInt(estimatedData.size()); + Set keys = estimatedData.keySet(); + for (String key : keys) { + dest.writeString(key); + encoder.encode(estimatedData.get(key), dest); + } + Bundle bundle = new Bundle(); + onSaveInstanceState(bundle); + dest.writeBundle(bundle); + } } + public final static Creator CREATOR = new Creator() { + @Override + public ParseObject createFromParcel(Parcel source) { + State state = State.createFromParcel(source); // Returns ParseUser.State if needed + ParseObject obj = create(state.className); // Returns the correct subclass + obj.setState(state); // This calls rebuildEstimatedData + if (source.readByte() == 1) obj.localId = source.readString(); + if (source.readByte() == 1) obj.isDeleted = true; + ParseParcelableDecoder decoder = ParseParcelableDecoder.get(); + int size = source.readInt(); + obj.estimatedData.clear(); // Clear estimatedData after setState. + for (int i = 0; i < size; i++) { + String key = source.readString(); + Object object = decoder.decode(source); + obj.estimatedData.put(key, object); + } + Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); + obj.onRestoreInstanceState(bundle); + return obj; + } + + @Override + public ParseObject[] newArray(int size) { + return new ParseObject[size]; + } + }; + + /** + * Called when parceling this ParseObject. + * Subclasses can put values into the provided {@link Bundle} and receive them later + * {@link #onRestoreInstanceState(Bundle)}. Note that internal fields are already parceled by + * the framework. + * + * @param outState Bundle to host extra values + */ + protected void onSaveInstanceState(Bundle outState) {} + + /** + * Called when unparceling this ParseObject. + * Subclasses can read values from the provided {@link Bundle} that were previously put + * during {@link #onSaveInstanceState(Bundle)}. At this point the internal state is already + * recovered. + * + * @param savedState Bundle to read the values from + */ + protected void onRestoreInstanceState(Bundle savedState) {} } diff --git a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java new file mode 100644 index 000000000..096288bfa --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A {@code ParseParcelableDecoder} can be used to unparcel objects such as {@link ParseObjects} + * from a {@link android.os.Parcel}. + * + * @see com.parse.ParseParcelableEncoder + */ + +/** package */ class ParseParcelableDecoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelableDecoder INSTANCE = new ParseParcelableDecoder(); + public static ParseParcelableDecoder get() { + return INSTANCE; + } + + public Object decode(Parcel source) { + String type = source.readString(); + switch (type) { + + case ParseParcelableEncoder.TYPE_OBJECT: + return source.readParcelable(ParseObject.class.getClassLoader()); + + case ParseParcelableEncoder.TYPE_DATE: + String iso = source.readString(); + return ParseDateFormat.getInstance().parse(iso); + + case ParseParcelableEncoder.TYPE_BYTES: + byte[] bytes = new byte[source.readInt()]; + source.readByteArray(bytes); + return bytes; + + case ParseParcelableEncoder.TYPE_ACL: + return source.readParcelable(ParseACL.class.getClassLoader()); + + case ParseParcelableEncoder.TYPE_MAP: + int size = source.readInt(); + Map map = new HashMap<>(size); + for (int i = 0; i < size; i++) { + map.put(source.readString(), decode(source)); + } + return map; + + case ParseParcelableEncoder.TYPE_COLLECTION: + int length = source.readInt(); + List list = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + list.add(i, decode(source)); + } + return list; + + case ParseParcelableEncoder.TYPE_JSON_NULL: + return JSONObject.NULL; + + case ParseParcelableEncoder.TYPE_NULL: + return null; + + case ParseParcelableEncoder.TYPE_NATIVE: + return source.readValue(null); // No need for a class loader. + + default: + throw new RuntimeException("Could not unparcel objects from this Parcel."); + + } + } + +} diff --git a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java new file mode 100644 index 000000000..5b8381550 --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2015-present, Parse, LLC. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +package com.parse; + +import android.os.Parcel; +import android.util.Base64; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * A {@code ParseParcelableEncoder} can be used to parcel objects such as {@link ParseObjects} + * into a {@link android.os.Parcel}. + * + * @see com.parse.ParseParcelableDecoder + */ + +/** package */ class ParseParcelableEncoder { + + // This class isn't really a Singleton, but since it has no state, it's more efficient to get the + // default instance. + private static final ParseParcelableEncoder INSTANCE = new ParseParcelableEncoder(); + public static ParseParcelableEncoder get() { + return INSTANCE; + } + + // TODO: remove this and user ParseEncoder.isValidType. + /* package */ static boolean isValidType(Object value) { + return value instanceof String + || value instanceof Number + || value instanceof Boolean + || value instanceof Date + || value instanceof List + || value instanceof Map + || value instanceof byte[] + || value == JSONObject.NULL + || value instanceof ParseObject + || value instanceof ParseACL; + // TODO: waiting merge || value instanceof ParseFile + // TODO: waiting merge || value instanceof ParseGeoPoint + // TODO: not done yet || value instanceof ParseRelation; + } + + /* package */ final static String TYPE_OBJECT = "ParseObject"; + /* package */ final static String TYPE_DATE = "Date"; + /* package */ final static String TYPE_BYTES = "Bytes"; + /* package */ final static String TYPE_ACL = "Acl"; + /* package */ final static String TYPE_MAP = "Map"; + /* package */ final static String TYPE_COLLECTION = "Collection"; + /* package */ final static String TYPE_JSON_NULL = "JsonNull"; + /* package */ final static String TYPE_NULL = "Null"; + /* package */ final static String TYPE_NATIVE = "Native"; + + public void encode(Object object, Parcel dest) { + try { + if (object instanceof ParseObject) { + dest.writeString(TYPE_OBJECT); + encodeParseObject((ParseObject) object, dest); + + } else if (object instanceof Date) { + dest.writeString(TYPE_DATE); + dest.writeString(ParseDateFormat.getInstance().format((Date) object)); + + } else if (object instanceof byte[]) { + dest.writeString(TYPE_BYTES); + byte[] bytes = (byte[]) object; + dest.writeInt(bytes.length); + dest.writeByteArray(bytes); + + } else if (object instanceof ParseFile) { // TODO + throw new IllegalArgumentException("Not supported yet"); + + } else if (object instanceof ParseGeoPoint) { // TODO + throw new IllegalArgumentException("Not supported yet"); + + } else if (object instanceof ParseACL) { + dest.writeString(TYPE_ACL); + dest.writeParcelable((ParseACL) object, 0); + + } else if (object instanceof Map) { + dest.writeString(TYPE_MAP); + @SuppressWarnings("unchecked") + Map map = (Map) object; + dest.writeInt(map.size()); + for (Map.Entry pair : map.entrySet()) { + dest.writeString(pair.getKey()); + encode(pair.getValue(), dest); + } + + } else if (object instanceof Collection) { + dest.writeString(TYPE_COLLECTION); + Collection collection = (Collection) object; + dest.writeInt(collection.size()); + for (Object item : collection) { + encode(item, dest); + } + + } else if (object instanceof ParseRelation) {// TODO + throw new IllegalArgumentException("Not supported yet."); + + } else if (object == JSONObject.NULL) { + dest.writeString(TYPE_JSON_NULL); + + } else if (object == null) { + dest.writeString(TYPE_NULL); + + // String, Number, Boolean. Simply use writeValue + } else if (isValidType(object)) { + dest.writeString(TYPE_NATIVE); + dest.writeValue(object); + + } else { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + + } catch (Exception e) { + throw new IllegalArgumentException("Could not encode this object into Parcel. " + + object.getClass().toString()); + } + } + + protected void encodeParseObject(ParseObject object, Parcel dest) { + dest.writeParcelable(object, 0); + } +} diff --git a/Parse/src/main/java/com/parse/ParseUser.java b/Parse/src/main/java/com/parse/ParseUser.java index 65d11bab9..ab29b4244 100644 --- a/Parse/src/main/java/com/parse/ParseUser.java +++ b/Parse/src/main/java/com/parse/ParseUser.java @@ -8,6 +8,9 @@ */ package com.parse; +import android.os.Bundle; +import android.os.Parcel; + import org.json.JSONObject; import java.util.ArrayList; @@ -124,6 +127,11 @@ private State(Builder builder) { isNew = builder.isNew; } + /* package */ State(Parcel source, String className) { + super(source, className); + isNew = source.readByte() == 1; + } + @SuppressWarnings("unchecked") @Override public Builder newBuilder() { @@ -150,6 +158,12 @@ public Map> authData() { public boolean isNew() { return isNew; } + + @Override + protected void writeToParcel(Parcel dest) { + super.writeToParcel(dest); + dest.writeByte(isNew ? (byte) 1 : 0); + } } // Whether the object is a currentUser. If so, it will always be persisted to disk on updates. @@ -1455,6 +1469,24 @@ public static void enableAutomaticUser() { //endregion + //region Parcelable + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + synchronized (mutex) { + outState.putBoolean("_isCurrentUser", isCurrentUser); + } + } + + @Override + protected void onRestoreInstanceState(Bundle savedState) { + super.onRestoreInstanceState(savedState); + setIsCurrentUser(savedState.getBoolean("_isCurrentUser")); + } + + //endregion + //region Legacy/Revocable Session Tokens /** diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index e4c906301..98c3564db 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -116,6 +116,8 @@ public void testServerData() { assertNull(state.get("baz")); } + // TODO test parcelable + @Test public void testToString() { String string = new ParseObject.State.Builder("TestObject").build().toString(); diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 24aded591..701d385b4 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -15,6 +17,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.ArrayList; import java.util.Arrays; @@ -38,6 +43,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) public class ParseObjectTest { @Rule @@ -497,4 +504,57 @@ public void testGetLongWithWrongValue() throws Exception { } //endregion + + //region testParcelable + + @Test + public void testParcelable() throws Exception { + ParseObject object = new ParseObject("Test"); + object.isDeleted = true; + object.put("long", 200L); + object.put("double", 30D); + object.put("int", 50); + object.put("string", "test"); + object.put("collection", Arrays.asList("test1", "test2")); + ParseObject other = new ParseObject("Test"); + other.setObjectId("otherId"); + object.put("pointer", other); + Map map = new HashMap<>(); + map.put("key1", "value"); + map.put("key2", 50); + object.put("map", map); + object.put("date", new Date(200)); + byte[] bytes = new byte[2]; + object.put("bytes", bytes); + object.put("null", JSONObject.NULL); + ParseACL acl = new ParseACL(); + acl.setReadAccess("reader", true); + object.setACL(acl); + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); + assertEquals(newObject.getClassName(), object.getClassName()); + assertEquals(newObject.isDeleted, true); + assertEquals(newObject.getLong("long"), object.getLong("long")); + assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); + assertEquals(newObject.getInt("int"), object.getInt("int")); + assertEquals(newObject.getString("string"), object.getString("string")); + assertEquals(newObject.getList("collection"), object.getList("collection")); + assertEquals(newObject.getParseObject("pointer").getClassName(), other.getClassName()); + assertEquals(newObject.getParseObject("pointer").getObjectId(), other.getObjectId()); + assertEquals(newObject.getMap("map"), object.getMap("map")); + assertEquals(newObject.getDate("date"), object.getDate("date")); + assertEquals(newObject.getBytes("bytes").length, bytes.length); + assertEquals(newObject.get("null"), object.get("null")); + assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); + } + + // TODO test ParseGeoPoint and ParseFile after merge + // TODO test subclassing + + //endregion + } diff --git a/Parse/src/test/java/com/parse/ParseUserTest.java b/Parse/src/test/java/com/parse/ParseUserTest.java index f8eae0f22..eba053569 100644 --- a/Parse/src/test/java/com/parse/ParseUserTest.java +++ b/Parse/src/test/java/com/parse/ParseUserTest.java @@ -71,6 +71,8 @@ public void tearDown() throws Exception { Parse.disableLocalDatastore(); } + // TODO: test parcelable (isNew, isCurrentUser) + @Test public void testImmutableKeys() { ParseUser user = new ParseUser(); From df6fc0ee9e243953afb96289dfdffbd95acc4b45 Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 05:03:19 +0200 Subject: [PATCH 03/10] Parceling ParseFieldOperations instead of estimatedData --- .../java/com/parse/ParseAddOperation.java | 11 ++ .../com/parse/ParseAddUniqueOperation.java | 11 ++ .../java/com/parse/ParseDeleteOperation.java | 7 ++ .../java/com/parse/ParseFieldOperation.java | 119 +++++++++++++++++- .../com/parse/ParseIncrementOperation.java | 8 ++ .../src/main/java/com/parse/ParseObject.java | 36 ++++-- .../java/com/parse/ParseOperationSet.java | 27 ++++ .../com/parse/ParseParcelableDecoder.java | 13 +- .../com/parse/ParseParcelableEncoder.java | 15 +-- .../com/parse/ParseRelationOperation.java | 26 ++++ .../java/com/parse/ParseRemoveOperation.java | 11 ++ .../java/com/parse/ParseSetOperation.java | 9 ++ Parse/src/main/java/com/parse/ParseUser.java | 6 +- .../test/java/com/parse/ParseObjectTest.java | 11 ++ 14 files changed, 280 insertions(+), 30 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseAddOperation.java b/Parse/src/main/java/com/parse/ParseAddOperation.java index 8c7aab6d3..a3fa139b3 100644 --- a/Parse/src/main/java/com/parse/ParseAddOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -35,6 +37,15 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("Add"); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java index 38bd879f7..116e8da2b 100644 --- a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -37,6 +39,15 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("AddUnique"); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override @SuppressWarnings("unchecked") public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { diff --git a/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/Parse/src/main/java/com/parse/ParseDeleteOperation.java index b6c9d335d..e10585d46 100644 --- a/Parse/src/main/java/com/parse/ParseDeleteOperation.java +++ b/Parse/src/main/java/com/parse/ParseDeleteOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -32,6 +34,11 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("Delete"); + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { return this; } diff --git a/Parse/src/main/java/com/parse/ParseFieldOperation.java b/Parse/src/main/java/com/parse/ParseFieldOperation.java index e83e0103b..8e61f04e5 100644 --- a/Parse/src/main/java/com/parse/ParseFieldOperation.java +++ b/Parse/src/main/java/com/parse/ParseFieldOperation.java @@ -8,12 +8,15 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.json.JSONArray; import org.json.JSONException; @@ -35,6 +38,16 @@ */ Object encode(ParseEncoder objectEncoder) throws JSONException; + /** + * Writes the ParseFieldOperation to the given Parcel using the given encoder. + * + * @param dest + * The destination Parcel. + * @param parcelableEncoder + * A ParseParcelableEncoder. + */ + void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder); + /** * Returns a field operation that is composed of a previous operation followed by this operation. * This will not mutate either operation. However, it may return self if the current operation is @@ -73,10 +86,11 @@ private ParseFieldOperations() { } /** - * A function that creates a ParseFieldOperation from a JSONObject. + * A function that creates a ParseFieldOperation from a JSONObject or a Parcel. */ private interface ParseFieldOperationFactory { ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException; + ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder); } // A map of all known decoders. @@ -106,6 +120,14 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } return op; } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + // Decode AddRelation and then RemoveRelation + ParseFieldOperation add = ParseFieldOperations.decode(source, decoder); + ParseFieldOperation remove = ParseFieldOperations.decode(source, decoder); + return remove.mergeWithPrevious(add); + } }); registerDecoder("Delete", new ParseFieldOperationFactory() { @@ -114,6 +136,11 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return ParseDeleteOperation.getInstance(); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + return ParseDeleteOperation.getInstance(); + } }); registerDecoder("Increment", new ParseFieldOperationFactory() { @@ -122,6 +149,11 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseIncrementOperation((Number) decoder.decode(object.opt("amount"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + return new ParseIncrementOperation((Number) decoder.decode(source)); + } }); registerDecoder("Add", new ParseFieldOperationFactory() { @@ -130,6 +162,16 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseAddOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseAddOperation(list); + } }); registerDecoder("AddUnique", new ParseFieldOperationFactory() { @@ -138,6 +180,16 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseAddUniqueOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseAddUniqueOperation(list); + } }); registerDecoder("Remove", new ParseFieldOperationFactory() { @@ -146,6 +198,16 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return new ParseRemoveOperation((Collection) decoder.decode(object.opt("objects"))); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + int size = source.readInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + list.set(i, decoder.decode(source)); + } + return new ParseRemoveOperation(list); + } }); registerDecoder("AddRelation", new ParseFieldOperationFactory() { @@ -156,6 +218,16 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) List objectsList = (List) decoder.decode(objectsArray); return new ParseRelationOperation<>(new HashSet<>(objectsList), null); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(set, null); + } }); registerDecoder("RemoveRelation", new ParseFieldOperationFactory() { @@ -166,15 +238,37 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) List objectsList = (List) decoder.decode(objectsArray); return new ParseRelationOperation<>(null, new HashSet<>(objectsList)); } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + int size = source.readInt(); + Set set = new HashSet<>(size); + for (int i = 0; i < size; i++) { + set.add((ParseObject) decoder.decode(source)); + } + return new ParseRelationOperation<>(null, set); + } + }); + + registerDecoder("Set", new ParseFieldOperationFactory() { + @Override + public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { + return null; // Not called. + } + + @Override + public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + return new ParseSetOperation(decoder.decode(source)); + } }); } /** - * Converts a parsed JSON object into a PFFieldOperation. + * Converts a parsed JSON object into a ParseFieldOperation. * * @param encoded * A JSONObject containing an __op field. - * @return A PFFieldOperation. + * @return A ParseFieldOperation. */ static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) throws JSONException { String op = encoded.optString("__op"); @@ -185,6 +279,25 @@ static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) thro return factory.decode(encoded, decoder); } + /** + * Reads a ParseFieldOperation out of the given Parcel. + * + * @param source + * The source Parcel. + * @param decoder + * The given ParseParcelableDecoder. + * + * @return A ParseFieldOperation. + */ + static ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + String op = source.readString(); + ParseFieldOperationFactory factory = opDecoderMap.get(op); + if (factory == null) { + throw new RuntimeException("Unable to decode operation of type " + op); + } + return factory.decode(source, decoder); + } + /** * Converts a JSONArray into an ArrayList. */ diff --git a/Parse/src/main/java/com/parse/ParseIncrementOperation.java b/Parse/src/main/java/com/parse/ParseIncrementOperation.java index 7c39b231d..82223881f 100644 --- a/Parse/src/main/java/com/parse/ParseIncrementOperation.java +++ b/Parse/src/main/java/com/parse/ParseIncrementOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -30,6 +32,12 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("Increment"); + parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index b1d817ac9..11883c96a 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -11,6 +11,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.Log; import org.json.JSONArray; import org.json.JSONException; @@ -53,6 +54,7 @@ public class ParseObject implements Parcelable { private static final String AUTO_CLASS_NAME = "_Automatic"; /* package */ static final String VERSION_NAME = "1.15.2-SNAPSHOT"; + private static final String TAG = "ParseObject"; /* REST JSON Keys @@ -4242,12 +4244,26 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte(localId != null ? (byte) 1 : 0); if (localId != null) dest.writeString(localId); dest.writeByte(isDeleted ? (byte) 1 : 0); - dest.writeInt(estimatedData.size()); - Set keys = estimatedData.keySet(); - for (String key : keys) { - dest.writeString(key); - encoder.encode(estimatedData.get(key), dest); + // Squash the operations queue if needed. + ParseOperationSet set; + if (hasOutstandingOperations()) { // There's more than one set. + Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + + "going on. The unparceled object will act as if these tasks had failed. This " + + "means that the subsequent call to save() will update again the same keys. " + + "This is dangerous for certain operations, like increment() and decrement(). " + + "To avoid inconsistencies, wait for save operations to end before parceling."); + ListIterator iterator = operationSetQueue.listIterator(); + set = new ParseOperationSet(); + while (iterator.hasNext()) { + ParseOperationSet other = iterator.next(); + other.mergeFrom(set); + set = other; + } + } else { + set = operationSetQueue.getLast(); } + set.setIsSaveEventually(false); + set.toParcel(dest, encoder); Bundle bundle = new Bundle(); onSaveInstanceState(bundle); dest.writeBundle(bundle); @@ -4263,12 +4279,10 @@ public ParseObject createFromParcel(Parcel source) { if (source.readByte() == 1) obj.localId = source.readString(); if (source.readByte() == 1) obj.isDeleted = true; ParseParcelableDecoder decoder = ParseParcelableDecoder.get(); - int size = source.readInt(); - obj.estimatedData.clear(); // Clear estimatedData after setState. - for (int i = 0; i < size; i++) { - String key = source.readString(); - Object object = decoder.decode(source); - obj.estimatedData.put(key, object); + ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); + for (String key : set.keySet()) { + ParseFieldOperation op = set.get(key); + obj.performOperation(key, op); // Update ops and estimatedData } Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); obj.onRestoreInstanceState(bundle); diff --git a/Parse/src/main/java/com/parse/ParseOperationSet.java b/Parse/src/main/java/com/parse/ParseOperationSet.java index 3189841ce..f34753c74 100644 --- a/Parse/src/main/java/com/parse/ParseOperationSet.java +++ b/Parse/src/main/java/com/parse/ParseOperationSet.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONException; import org.json.JSONObject; @@ -139,4 +141,29 @@ public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) return operationSet; } + + /** + * Parcels this operation set into a Parcel with the given encoder. + */ + /* package */ void toParcel(Parcel dest, ParseParcelableEncoder encoder) { + dest.writeString(uuid); + dest.writeByte(isSaveEventually ? (byte) 1 : 0); + dest.writeInt(size()); + for (String key : keySet()) { + dest.writeString(key); + encoder.encode(get(key), dest); + } + } + + /* package */ static ParseOperationSet fromParcel(Parcel source, ParseParcelableDecoder decoder) { + ParseOperationSet set = new ParseOperationSet(source.readString()); + set.setIsSaveEventually(source.readByte() == 1); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + String key = source.readString(); + ParseFieldOperation op = (ParseFieldOperation) decoder.decode(source); + set.put(key, op); + } + return set; + } } diff --git a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java index 096288bfa..0569f90a8 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java @@ -13,21 +13,17 @@ import org.json.JSONObject; import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; /** - * A {@code ParseParcelableDecoder} can be used to unparcel objects such as {@link ParseObjects} - * from a {@link android.os.Parcel}. + * A {@code ParseParcelableDecoder} can be used to unparcel objects such as + * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. * * @see com.parse.ParseParcelableEncoder */ - -/** package */ class ParseParcelableDecoder { +/* package */ class ParseParcelableDecoder { // This class isn't really a Singleton, but since it has no state, it's more efficient to get the // default instance. @@ -52,6 +48,9 @@ public Object decode(Parcel source) { source.readByteArray(bytes); return bytes; + case ParseParcelableEncoder.TYPE_OP: + return ParseFieldOperations.decode(source, this); + case ParseParcelableEncoder.TYPE_ACL: return source.readParcelable(ParseACL.class.getClassLoader()); diff --git a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java index 5b8381550..8ea7ee5b1 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java @@ -9,10 +9,7 @@ package com.parse; import android.os.Parcel; -import android.util.Base64; -import org.json.JSONArray; -import org.json.JSONException; import org.json.JSONObject; import java.util.Collection; @@ -21,13 +18,12 @@ import java.util.Map; /** - * A {@code ParseParcelableEncoder} can be used to parcel objects such as {@link ParseObjects} - * into a {@link android.os.Parcel}. + * A {@code ParseParcelableEncoder} can be used to parcel objects such as + * {@link com.parse.ParseObject} into a {@link android.os.Parcel}. * * @see com.parse.ParseParcelableDecoder */ - -/** package */ class ParseParcelableEncoder { +/* package */ class ParseParcelableEncoder { // This class isn't really a Singleton, but since it has no state, it's more efficient to get the // default instance. @@ -62,6 +58,7 @@ public static ParseParcelableEncoder get() { /* package */ final static String TYPE_JSON_NULL = "JsonNull"; /* package */ final static String TYPE_NULL = "Null"; /* package */ final static String TYPE_NATIVE = "Native"; + /* package */ final static String TYPE_OP = "Operation"; public void encode(Object object, Parcel dest) { try { @@ -79,6 +76,10 @@ public void encode(Object object, Parcel dest) { dest.writeInt(bytes.length); dest.writeByteArray(bytes); + } else if (object instanceof ParseFieldOperation) { + dest.writeString(TYPE_OP); + ((ParseFieldOperation) object).encode(dest, this); + } else if (object instanceof ParseFile) { // TODO throw new IllegalArgumentException("Not supported yet"); diff --git a/Parse/src/main/java/com/parse/ParseRelationOperation.java b/Parse/src/main/java/com/parse/ParseRelationOperation.java index 31cc0230a..5771d2fe2 100644 --- a/Parse/src/main/java/com/parse/ParseRelationOperation.java +++ b/Parse/src/main/java/com/parse/ParseRelationOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -187,6 +189,30 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + if (relationsToAdd.isEmpty() && relationsToRemove.isEmpty()) { + throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); + } + if (relationsToAdd.size() > 0 && relationsToRemove.size() > 0) { + dest.writeString("Batch"); + } + if (relationsToAdd.size() > 0) { + dest.writeString("AddRelation"); + dest.writeInt(relationsToAdd.size()); + for (ParseObject object : relationsToAdd) { + parcelableEncoder.encode(object, dest); + } + } + if (relationsToRemove.size() > 0) { + dest.writeString("RemoveRelation"); + dest.writeInt(relationsToRemove.size()); + for (ParseObject object : relationsToRemove) { + parcelableEncoder.encode(object, dest); + } + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseRemoveOperation.java b/Parse/src/main/java/com/parse/ParseRemoveOperation.java index 8f6d7b73e..cda8708d8 100644 --- a/Parse/src/main/java/com/parse/ParseRemoveOperation.java +++ b/Parse/src/main/java/com/parse/ParseRemoveOperation.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -37,6 +39,15 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("Remove"); + dest.writeInt(objects.size()); + for (Object object : objects) { + parcelableEncoder.encode(object, dest); + } + } + + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { if (previous == null) { return this; diff --git a/Parse/src/main/java/com/parse/ParseSetOperation.java b/Parse/src/main/java/com/parse/ParseSetOperation.java index 00ecaa57c..9b5b6418e 100644 --- a/Parse/src/main/java/com/parse/ParseSetOperation.java +++ b/Parse/src/main/java/com/parse/ParseSetOperation.java @@ -11,6 +11,9 @@ /** * An operation where a field is set to a given value regardless of its previous value. */ + +import android.os.Parcel; + /** package */ class ParseSetOperation implements ParseFieldOperation { private final Object value; @@ -27,6 +30,12 @@ public Object encode(ParseEncoder objectEncoder) { return objectEncoder.encode(value); } + @Override + public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + dest.writeString("Set"); + parcelableEncoder.encode(value, dest); + } + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { return this; diff --git a/Parse/src/main/java/com/parse/ParseUser.java b/Parse/src/main/java/com/parse/ParseUser.java index ab29b4244..9dc8f9d0e 100644 --- a/Parse/src/main/java/com/parse/ParseUser.java +++ b/Parse/src/main/java/com/parse/ParseUser.java @@ -41,6 +41,8 @@ public class ParseUser extends ParseObject { private static final List READ_ONLY_KEYS = Collections.unmodifiableList( Arrays.asList(KEY_SESSION_TOKEN, KEY_AUTH_DATA)); + private static final String PARCEL_KEY_IS_CURRENT_USER = "_isCurrentUser"; + /** * Constructs a query for {@code ParseUser}. * @@ -1475,14 +1477,14 @@ public static void enableAutomaticUser() { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); synchronized (mutex) { - outState.putBoolean("_isCurrentUser", isCurrentUser); + outState.putBoolean(PARCEL_KEY_IS_CURRENT_USER, isCurrentUser); } } @Override protected void onRestoreInstanceState(Bundle savedState) { super.onRestoreInstanceState(savedState); - setIsCurrentUser(savedState.getBoolean("_isCurrentUser")); + setIsCurrentUser(savedState.getBoolean(PARCEL_KEY_IS_CURRENT_USER, false)); } //endregion diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 701d385b4..258440b22 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -509,6 +509,7 @@ public void testGetLongWithWrongValue() throws Exception { @Test public void testParcelable() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); ParseObject object = new ParseObject("Test"); object.isDeleted = true; object.put("long", 200L); @@ -538,6 +539,7 @@ public void testParcelable() throws Exception { ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); assertEquals(newObject.getClassName(), object.getClassName()); assertEquals(newObject.isDeleted, true); + assertEquals(newObject.hasChanges(), true); assertEquals(newObject.getLong("long"), object.getLong("long")); assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); assertEquals(newObject.getInt("int"), object.getInt("int")); @@ -552,6 +554,15 @@ public void testParcelable() throws Exception { assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); } + @Test + public void testRecursiveParcel() throws Exception { + ParseObject object = new ParseObject("Test"); + object.put("itself", object); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + // TODO fix this + } + // TODO test ParseGeoPoint and ParseFile after merge // TODO test subclassing From 0b3a8c41e897f7c5d69b1b2d022fd57d1614f9a8 Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 14:34:59 +0200 Subject: [PATCH 04/10] Name constants for operations --- .../java/com/parse/ParseAddOperation.java | 6 +++-- .../com/parse/ParseAddUniqueOperation.java | 6 +++-- .../java/com/parse/ParseDeleteOperation.java | 6 +++-- .../java/com/parse/ParseFieldOperation.java | 22 +++++++++---------- .../com/parse/ParseIncrementOperation.java | 6 +++-- .../src/main/java/com/parse/ParseObject.java | 4 ---- .../com/parse/ParseRelationOperation.java | 16 +++++++++----- .../java/com/parse/ParseRemoveOperation.java | 6 +++-- .../java/com/parse/ParseSetOperation.java | 4 +++- 9 files changed, 44 insertions(+), 32 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseAddOperation.java b/Parse/src/main/java/com/parse/ParseAddOperation.java index a3fa139b3..54a226909 100644 --- a/Parse/src/main/java/com/parse/ParseAddOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddOperation.java @@ -22,6 +22,8 @@ * An operation that adds a new element to an array field. */ /** package */ class ParseAddOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Add"; + protected final ArrayList objects = new ArrayList<>(); public ParseAddOperation(Collection coll) { @@ -31,14 +33,14 @@ public ParseAddOperation(Collection coll) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Add"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(objects)); return output; } @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("Add"); + dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { parcelableEncoder.encode(object, dest); diff --git a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java index 116e8da2b..c7a0e85ef 100644 --- a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java @@ -24,6 +24,8 @@ * An operation that adds a new element to an array field, only if it wasn't already present. */ /** package */ class ParseAddUniqueOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "AddUnique"; + protected final LinkedHashSet objects = new LinkedHashSet<>(); public ParseAddUniqueOperation(Collection col) { @@ -33,14 +35,14 @@ public ParseAddUniqueOperation(Collection col) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "AddUnique"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); return output; } @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("AddUnique"); + dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { parcelableEncoder.encode(object, dest); diff --git a/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/Parse/src/main/java/com/parse/ParseDeleteOperation.java index e10585d46..fc38f938b 100644 --- a/Parse/src/main/java/com/parse/ParseDeleteOperation.java +++ b/Parse/src/main/java/com/parse/ParseDeleteOperation.java @@ -17,6 +17,8 @@ * An operation where a field is deleted from the object. */ /** package */ class ParseDeleteOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Delete"; + private static final ParseDeleteOperation defaultInstance = new ParseDeleteOperation(); public static ParseDeleteOperation getInstance() { @@ -29,13 +31,13 @@ private ParseDeleteOperation() { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Delete"); + output.put("__op", OP_NAME); return output; } @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("Delete"); + dest.writeString(OP_NAME); } @Override diff --git a/Parse/src/main/java/com/parse/ParseFieldOperation.java b/Parse/src/main/java/com/parse/ParseFieldOperation.java index 8e61f04e5..2d254e208 100644 --- a/Parse/src/main/java/com/parse/ParseFieldOperation.java +++ b/Parse/src/main/java/com/parse/ParseFieldOperation.java @@ -104,11 +104,11 @@ private static void registerDecoder(String opName, ParseFieldOperationFactory fa } /** - * Registers a list of default decoder functions that convert a JSONObject with an __op field into - * a ParseFieldOperation. + * Registers a list of default decoder functions that convert a JSONObject with an __op field, + * or a Parcel with a op name string, into a ParseFieldOperation. */ static void registerDefaultDecoders() { - registerDecoder("Batch", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_BATCH, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -130,7 +130,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("Delete", new ParseFieldOperationFactory() { + registerDecoder(ParseDeleteOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -143,7 +143,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("Increment", new ParseFieldOperationFactory() { + registerDecoder(ParseIncrementOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -156,7 +156,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("Add", new ParseFieldOperationFactory() { + registerDecoder(ParseAddOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -174,7 +174,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("AddUnique", new ParseFieldOperationFactory() { + registerDecoder(ParseAddUniqueOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -192,7 +192,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("Remove", new ParseFieldOperationFactory() { + registerDecoder(ParseRemoveOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -210,7 +210,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("AddRelation", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_ADD, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -230,7 +230,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("RemoveRelation", new ParseFieldOperationFactory() { + registerDecoder(ParseRelationOperation.OP_NAME_REMOVE, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { @@ -250,7 +250,7 @@ public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) } }); - registerDecoder("Set", new ParseFieldOperationFactory() { + registerDecoder(ParseSetOperation.OP_NAME, new ParseFieldOperationFactory() { @Override public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException { return null; // Not called. diff --git a/Parse/src/main/java/com/parse/ParseIncrementOperation.java b/Parse/src/main/java/com/parse/ParseIncrementOperation.java index 82223881f..73eabd2f8 100644 --- a/Parse/src/main/java/com/parse/ParseIncrementOperation.java +++ b/Parse/src/main/java/com/parse/ParseIncrementOperation.java @@ -17,6 +17,8 @@ * An operation that increases a numeric field's value by a given amount. */ /** package */ class ParseIncrementOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Increment"; + private final Number amount; public ParseIncrementOperation(Number amount) { @@ -26,14 +28,14 @@ public ParseIncrementOperation(Number amount) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Increment"); + output.put("__op", OP_NAME); output.put("amount", amount); return output; } @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("Increment"); + dest.writeString(OP_NAME); parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 11883c96a..3839effb9 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -4234,10 +4234,6 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - // TODO operationSetQueue? - // TODO isDeletingEventually? - // TODO warn if it has ongoing tasks - ParseParcelableEncoder encoder = ParseParcelableEncoder.get(); synchronized (mutex) { state.writeToParcel(dest); diff --git a/Parse/src/main/java/com/parse/ParseRelationOperation.java b/Parse/src/main/java/com/parse/ParseRelationOperation.java index 5771d2fe2..d26122761 100644 --- a/Parse/src/main/java/com/parse/ParseRelationOperation.java +++ b/Parse/src/main/java/com/parse/ParseRelationOperation.java @@ -22,6 +22,10 @@ * An operation where a ParseRelation's value is modified. */ /** package */ class ParseRelationOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME_ADD = "AddRelation"; + /* package */ final static String OP_NAME_REMOVE = "RemoveRelation"; + /* package */ final static String OP_NAME_BATCH = "Batch"; + // The className of the target objects. private final String targetClass; @@ -157,19 +161,19 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { if (relationsToAdd.size() > 0) { adds = new JSONObject(); - adds.put("__op", "AddRelation"); + adds.put("__op", OP_NAME_ADD); adds.put("objects", convertSetToArray(relationsToAdd, objectEncoder)); } if (relationsToRemove.size() > 0) { removes = new JSONObject(); - removes.put("__op", "RemoveRelation"); + removes.put("__op", OP_NAME_REMOVE); removes.put("objects", convertSetToArray(relationsToRemove, objectEncoder)); } if (adds != null && removes != null) { JSONObject result = new JSONObject(); - result.put("__op", "Batch"); + result.put("__op", OP_NAME_BATCH); JSONArray ops = new JSONArray(); ops.put(adds); ops.put(removes); @@ -194,17 +198,17 @@ public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); } if (relationsToAdd.size() > 0 && relationsToRemove.size() > 0) { - dest.writeString("Batch"); + dest.writeString(OP_NAME_BATCH); } if (relationsToAdd.size() > 0) { - dest.writeString("AddRelation"); + dest.writeString(OP_NAME_ADD); dest.writeInt(relationsToAdd.size()); for (ParseObject object : relationsToAdd) { parcelableEncoder.encode(object, dest); } } if (relationsToRemove.size() > 0) { - dest.writeString("RemoveRelation"); + dest.writeString(OP_NAME_REMOVE); dest.writeInt(relationsToRemove.size()); for (ParseObject object : relationsToRemove) { parcelableEncoder.encode(object, dest); diff --git a/Parse/src/main/java/com/parse/ParseRemoveOperation.java b/Parse/src/main/java/com/parse/ParseRemoveOperation.java index cda8708d8..89b660ec4 100644 --- a/Parse/src/main/java/com/parse/ParseRemoveOperation.java +++ b/Parse/src/main/java/com/parse/ParseRemoveOperation.java @@ -24,6 +24,8 @@ * An operation that removes every instance of an element from an array field. */ /** package */ class ParseRemoveOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Remove"; + protected final HashSet objects = new HashSet<>(); public ParseRemoveOperation(Collection coll) { @@ -33,14 +35,14 @@ public ParseRemoveOperation(Collection coll) { @Override public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { JSONObject output = new JSONObject(); - output.put("__op", "Remove"); + output.put("__op", OP_NAME); output.put("objects", objectEncoder.encode(new ArrayList<>(objects))); return output; } @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("Remove"); + dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { parcelableEncoder.encode(object, dest); diff --git a/Parse/src/main/java/com/parse/ParseSetOperation.java b/Parse/src/main/java/com/parse/ParseSetOperation.java index 9b5b6418e..fc5bf7907 100644 --- a/Parse/src/main/java/com/parse/ParseSetOperation.java +++ b/Parse/src/main/java/com/parse/ParseSetOperation.java @@ -15,6 +15,8 @@ import android.os.Parcel; /** package */ class ParseSetOperation implements ParseFieldOperation { + /* package */ final static String OP_NAME = "Set"; + private final Object value; public ParseSetOperation(Object newValue) { @@ -32,7 +34,7 @@ public Object encode(ParseEncoder objectEncoder) { @Override public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { - dest.writeString("Set"); + dest.writeString(OP_NAME); parcelableEncoder.encode(value, dest); } From 24f5cc3463e16076c40af3bf8e0315763f2dfdbb Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 16:18:46 +0200 Subject: [PATCH 05/10] Tests for ParseObject, ParseUser, ParseACL --- Parse/src/main/java/com/parse/ParseACL.java | 23 +++++++--- .../src/main/java/com/parse/ParseObject.java | 2 +- .../src/test/java/com/parse/ParseACLTest.java | 29 ++++++++++--- .../test/java/com/parse/ParseFileTest.java | 3 +- .../java/com/parse/ParseObjectStateTest.java | 42 ++++++++++++++++++- .../test/java/com/parse/ParseObjectTest.java | 2 +- .../test/java/com/parse/ParseUserTest.java | 36 +++++++++++++++- 7 files changed, 118 insertions(+), 19 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseACL.java b/Parse/src/main/java/com/parse/ParseACL.java index b9ceffe52..349e3a777 100644 --- a/Parse/src/main/java/com/parse/ParseACL.java +++ b/Parse/src/main/java/com/parse/ParseACL.java @@ -218,7 +218,7 @@ public ParseACL(ParseUser owner) { } /* package for tests */ void resolveUser(ParseUser user) { - if (user != unresolvedUser) { + if (!isUnresolvedUser(user)) { return; } if (permissionsById.containsKey(UNRESOLVED_KEY)) { @@ -349,20 +349,27 @@ private void setUnresolvedWriteAccess(ParseUser user, boolean allowed) { private void prepareUnresolvedUser(ParseUser user) { // Registers a listener for the user so that when it is saved, the // unresolved ACL will be resolved. - if (this.unresolvedUser != user) { + if (!isUnresolvedUser(user)) { permissionsById.remove(UNRESOLVED_KEY); unresolvedUser = user; - user.registerSaveListener(new UserResolutionListener(this)); + unresolvedUser.registerSaveListener(new UserResolutionListener(this)); } } + private boolean isUnresolvedUser(ParseUser other) { + // This might be a different instance, but if they have the same local id, assume it's correct. + if (other == null || unresolvedUser == null) return false; + return other == unresolvedUser || (other.getObjectId() == null && + other.getOrCreateLocalId().equals(unresolvedUser.getOrCreateLocalId())); + } + /** * Get whether the given user id is *explicitly* allowed to read this object. Even if this returns * {@code false}, the user may still be able to access it if getPublicReadAccess returns * {@code true} or a role that the user belongs to has read access. */ public boolean getReadAccess(ParseUser user) { - if (user == unresolvedUser) { + if (isUnresolvedUser(user)) { return getReadAccess(UNRESOLVED_KEY); } if (user.isLazy()) { @@ -394,7 +401,7 @@ public void setWriteAccess(ParseUser user, boolean allowed) { * {@code true} or a role that the user belongs to has write access. */ public boolean getWriteAccess(ParseUser user) { - if (user == unresolvedUser) { + if (isUnresolvedUser(user)) { return getWriteAccess(UNRESOLVED_KEY); } if (user.isLazy()) { @@ -566,7 +573,11 @@ public void writeToParcel(Parcel dest, int flags) { permissions.toParcel(dest); } dest.writeByte(unresolvedUser != null ? (byte) 1 : 0); - if (unresolvedUser != null) dest.writeParcelable(unresolvedUser, 0); + if (unresolvedUser != null) { + // Ensure it has a local Id so we recognize it after parceling + unresolvedUser.getOrCreateLocalId(); + dest.writeParcelable(unresolvedUser, 0); + } } public final static Creator CREATOR = new Creator() { diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 3839effb9..799d53e35 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -384,7 +384,7 @@ public String toString() { // Cached State private final Map estimatedData; - private String localId; + /* package */ String localId; private final ParseMulticastDelegate saveEvent = new ParseMulticastDelegate<>(); /* package */ boolean isDeleted; diff --git a/Parse/src/test/java/com/parse/ParseACLTest.java b/Parse/src/test/java/com/parse/ParseACLTest.java index dae17c293..dde0d931d 100644 --- a/Parse/src/test/java/com/parse/ParseACLTest.java +++ b/Parse/src/test/java/com/parse/ParseACLTest.java @@ -16,12 +16,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import org.skyscreamer.jsonassert.JSONCompareMode; import java.util.HashMap; import java.util.Map; @@ -35,6 +31,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -194,7 +191,23 @@ public void testParcelable() throws Exception { assertFalse(acl.getPublicWriteAccess()); } - // TODO testParcelableWithUnresolvedUser once we have decent User parceling. + @Test + public void testParcelableWithUnresolvedUser() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); // Needed for unparceling ParseObjects + ParseACL acl = new ParseACL(); + ParseUser unresolved = new ParseUser(); + setLazy(unresolved); + acl.setReadAccess(unresolved, true); + + // unresolved users need a local id when parcelling. + // Since we don't have an Android environment, local id creation will fail. + unresolved.localId = "local_12hs2"; + Parcel parcel = Parcel.obtain(); + acl.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + acl = ParseACL.CREATOR.createFromParcel(parcel); + assertTrue(acl.getReadAccess(unresolved)); + } //endregion @@ -233,7 +246,11 @@ public void testResolveUserWithNewUser() throws Exception { ParseACL acl = new ParseACL(); acl.setReadAccess(unresolvedUser, true); - acl.resolveUser(new ParseUser()); + ParseUser other = new ParseUser(); + // local id creation fails if we don't have Android environment + unresolvedUser.localId = "someId"; + other.localId = "someOtherId"; + acl.resolveUser(other); // Make sure unresolvedUser is not changed assertSame(unresolvedUser, acl.getUnresolvedUser()); diff --git a/Parse/src/test/java/com/parse/ParseFileTest.java b/Parse/src/test/java/com/parse/ParseFileTest.java index 6afa4829b..d9776262c 100644 --- a/Parse/src/test/java/com/parse/ParseFileTest.java +++ b/Parse/src/test/java/com/parse/ParseFileTest.java @@ -43,7 +43,8 @@ public class ParseFileTest { @Before public void setup() { - ParseTestUtils.setTestParseUser(); + ParseCorePlugins.getInstance().reset(); + ParseTestUtils.setTestParseUser(); } @After diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index 98c3564db..ced5b4114 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -8,7 +8,12 @@ */ package com.parse; +import android.os.Parcel; + import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import java.util.Arrays; import java.util.Date; @@ -19,6 +24,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) public class ParseObjectStateTest { @Test @@ -79,6 +86,39 @@ public void testCopy() { assertTrue(copy.availableKeys().containsAll(state.availableKeys())); } + @Test + public void testParcelable() { + long updatedAt = System.currentTimeMillis(); + long createdAt = updatedAt + 10; + + ParseObject.State state = new ParseObject.State.Builder("TestObject") + .objectId("fake") + .createdAt(new Date(createdAt)) + .updatedAt(new Date(updatedAt)) + .isComplete(true) + .put("foo", "bar") + .put("baz", "qux") + .availableKeys(Arrays.asList("safe", "keys")) + .build(); + + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel); + parcel.setDataPosition(0); + ParseObject.State copy = ParseObject.State.createFromParcel(parcel); + + assertEquals(state.className(), copy.className()); + assertEquals(state.objectId(), copy.objectId()); + assertEquals(state.createdAt(), copy.createdAt()); + assertEquals(state.updatedAt(), copy.updatedAt()); + assertEquals(state.isComplete(), copy.isComplete()); + assertEquals(state.keySet().size(), copy.keySet().size()); + assertEquals(state.get("foo"), copy.get("foo")); + assertEquals(state.get("baz"), copy.get("baz")); + assertEquals(state.availableKeys().size(), copy.availableKeys().size()); + assertTrue(state.availableKeys().containsAll(copy.availableKeys())); + assertTrue(copy.availableKeys().containsAll(state.availableKeys())); + } + @Test public void testAutomaticUpdatedAt() { long createdAt = System.currentTimeMillis(); @@ -116,8 +156,6 @@ public void testServerData() { assertNull(state.get("baz")); } - // TODO test parcelable - @Test public void testToString() { String string = new ParseObject.State.Builder("TestObject").build().toString(); diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 258440b22..1fe474abf 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -559,7 +559,7 @@ public void testRecursiveParcel() throws Exception { ParseObject object = new ParseObject("Test"); object.put("itself", object); Parcel parcel = Parcel.obtain(); - object.writeToParcel(parcel, 0); + // object.writeToParcel(parcel, 0); // TODO fix this } diff --git a/Parse/src/test/java/com/parse/ParseUserTest.java b/Parse/src/test/java/com/parse/ParseUserTest.java index eba053569..26e4176ce 100644 --- a/Parse/src/test/java/com/parse/ParseUserTest.java +++ b/Parse/src/test/java/com/parse/ParseUserTest.java @@ -8,6 +8,8 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONObject; import org.junit.After; import org.junit.Before; @@ -71,8 +73,6 @@ public void tearDown() throws Exception { Parse.disableLocalDatastore(); } - // TODO: test parcelable (isNew, isCurrentUser) - @Test public void testImmutableKeys() { ParseUser user = new ParseUser(); @@ -97,6 +97,38 @@ public void testImmutableKeys() { } } + // region Parcelable + + @Test + public void testOnSaveRestoreState() throws Exception { + ParseUser user = new ParseUser(); + user.setIsCurrentUser(true); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isCurrentUser()); + } + + @Test + public void testParcelableState() throws Exception { + ParseUser.State state = new ParseUser.State.Builder() + .objectId("test") + .isNew(true) + .build(); + ParseUser user = ParseObject.from(state); + assertTrue(user.isNew()); + + Parcel parcel = Parcel.obtain(); + user.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + user = (ParseUser) ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(user.isNew()); + } + + // endregion + //region SignUpAsync @Test From e1a5bab75fded2e119d99d0eecef0c007bfc01c2 Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 20:33:44 +0200 Subject: [PATCH 06/10] Ability to pass encoders/decoders down the object tree --- Parse/src/main/java/com/parse/ParseACL.java | 12 ++-- .../src/main/java/com/parse/ParseObject.java | 36 +++++----- .../com/parse/ParseParcelableDecoder.java | 7 +- .../com/parse/ParseParcelableEncoder.java | 13 ++-- .../main/java/com/parse/ParseRelation.java | 65 ++++++++++++++++++- .../test/java/com/parse/ParseFileTest.java | 4 +- 6 files changed, 108 insertions(+), 29 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseACL.java b/Parse/src/main/java/com/parse/ParseACL.java index 349e3a777..32955069d 100644 --- a/Parse/src/main/java/com/parse/ParseACL.java +++ b/Parse/src/main/java/com/parse/ParseACL.java @@ -564,6 +564,10 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, ParseParcelableEncoder.get()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { dest.writeByte(shared ? (byte) 1 : 0); dest.writeInt(permissionsById.size()); Set keys = permissionsById.keySet(); @@ -576,14 +580,14 @@ public void writeToParcel(Parcel dest, int flags) { if (unresolvedUser != null) { // Ensure it has a local Id so we recognize it after parceling unresolvedUser.getOrCreateLocalId(); - dest.writeParcelable(unresolvedUser, 0); + encoder.encode(unresolvedUser, dest); } } public final static Creator CREATOR = new Creator() { @Override public ParseACL createFromParcel(Parcel source) { - return new ParseACL(source); + return new ParseACL(source, ParseParcelableDecoder.get()); } @Override @@ -592,7 +596,7 @@ public ParseACL[] newArray(int size) { } }; - /* package */ ParseACL(Parcel source) { + /* package */ ParseACL(Parcel source, ParseParcelableDecoder decoder) { shared = source.readByte() == 1; int size = source.readInt(); for (int i = 0; i < size; i++) { @@ -601,7 +605,7 @@ public ParseACL[] newArray(int size) { permissionsById.put(key, permissions); } if (source.readByte() == 1) { - unresolvedUser = source.readParcelable(ParseUser.class.getClassLoader()); + unresolvedUser = (ParseUser) decoder.decode(source); unresolvedUser.registerSaveListener(new UserResolutionListener(this)); } } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 799d53e35..14189c8a3 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -4234,7 +4234,10 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - ParseParcelableEncoder encoder = ParseParcelableEncoder.get(); + writeToParcel(dest, ParseParcelableEncoder.get()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { synchronized (mutex) { state.writeToParcel(dest); dest.writeByte(localId != null ? (byte) 1 : 0); @@ -4269,20 +4272,7 @@ public void writeToParcel(Parcel dest, int flags) { public final static Creator CREATOR = new Creator() { @Override public ParseObject createFromParcel(Parcel source) { - State state = State.createFromParcel(source); // Returns ParseUser.State if needed - ParseObject obj = create(state.className); // Returns the correct subclass - obj.setState(state); // This calls rebuildEstimatedData - if (source.readByte() == 1) obj.localId = source.readString(); - if (source.readByte() == 1) obj.isDeleted = true; - ParseParcelableDecoder decoder = ParseParcelableDecoder.get(); - ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); - for (String key : set.keySet()) { - ParseFieldOperation op = set.get(key); - obj.performOperation(key, op); // Update ops and estimatedData - } - Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); - obj.onRestoreInstanceState(bundle); - return obj; + return ParseObject.createFromParcel(source, ParseParcelableDecoder.get()); } @Override @@ -4291,6 +4281,22 @@ public ParseObject[] newArray(int size) { } }; + /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelableDecoder decoder) { + State state = State.createFromParcel(source); // Returns ParseUser.State if needed + ParseObject obj = create(state.className); // Returns the correct subclass + obj.setState(state); // This calls rebuildEstimatedData + if (source.readByte() == 1) obj.localId = source.readString(); + if (source.readByte() == 1) obj.isDeleted = true; + ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); + for (String key : set.keySet()) { + ParseFieldOperation op = set.get(key); + obj.performOperation(key, op); // Update ops and estimatedData + } + Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); + obj.onRestoreInstanceState(bundle); + return obj; + } + /** * Called when parceling this ParseObject. * Subclasses can put values into the provided {@link Bundle} and receive them later diff --git a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java index 0569f90a8..bfcac4707 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelableDecoder.java @@ -37,7 +37,7 @@ public Object decode(Parcel source) { switch (type) { case ParseParcelableEncoder.TYPE_OBJECT: - return source.readParcelable(ParseObject.class.getClassLoader()); + return ParseObject.createFromParcel(source, this); case ParseParcelableEncoder.TYPE_DATE: String iso = source.readString(); @@ -52,7 +52,10 @@ public Object decode(Parcel source) { return ParseFieldOperations.decode(source, this); case ParseParcelableEncoder.TYPE_ACL: - return source.readParcelable(ParseACL.class.getClassLoader()); + return new ParseACL(source, this); + + case ParseParcelableEncoder.TYPE_RELATION: + return new ParseRelation<>(source, this); case ParseParcelableEncoder.TYPE_MAP: int size = source.readInt(); diff --git a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java index 8ea7ee5b1..361d9ad95 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelableEncoder.java @@ -43,16 +43,17 @@ public static ParseParcelableEncoder get() { || value instanceof byte[] || value == JSONObject.NULL || value instanceof ParseObject - || value instanceof ParseACL; + || value instanceof ParseACL // TODO: waiting merge || value instanceof ParseFile // TODO: waiting merge || value instanceof ParseGeoPoint - // TODO: not done yet || value instanceof ParseRelation; + || value instanceof ParseRelation; } /* package */ final static String TYPE_OBJECT = "ParseObject"; /* package */ final static String TYPE_DATE = "Date"; /* package */ final static String TYPE_BYTES = "Bytes"; /* package */ final static String TYPE_ACL = "Acl"; + /* package */ final static String TYPE_RELATION = "Relation"; /* package */ final static String TYPE_MAP = "Map"; /* package */ final static String TYPE_COLLECTION = "Collection"; /* package */ final static String TYPE_JSON_NULL = "JsonNull"; @@ -88,7 +89,11 @@ public void encode(Object object, Parcel dest) { } else if (object instanceof ParseACL) { dest.writeString(TYPE_ACL); - dest.writeParcelable((ParseACL) object, 0); + ((ParseACL) object).writeToParcel(dest, this); + + } else if (object instanceof ParseRelation) { + dest.writeString(TYPE_RELATION); + ((ParseRelation) object).writeToParcel(dest, this); } else if (object instanceof Map) { dest.writeString(TYPE_MAP); @@ -134,6 +139,6 @@ public void encode(Object object, Parcel dest) { } protected void encodeParseObject(ParseObject object, Parcel dest) { - dest.writeParcelable(object, 0); + object.writeToParcel(dest, this); } } diff --git a/Parse/src/main/java/com/parse/ParseRelation.java b/Parse/src/main/java/com/parse/ParseRelation.java index c9678ed9b..a54d69fa5 100644 --- a/Parse/src/main/java/com/parse/ParseRelation.java +++ b/Parse/src/main/java/com/parse/ParseRelation.java @@ -8,6 +8,9 @@ */ package com.parse; +import android.os.Parcel; +import android.os.Parcelable; + import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; @@ -21,7 +24,7 @@ * A class that is used to access all of the children of a many-to-many relationship. Each instance * of Parse.Relation is associated with a particular parent object and key. */ -public class ParseRelation { +public class ParseRelation implements Parcelable { private final Object mutex = new Object(); // The owning object of this ParseRelation. @@ -59,7 +62,7 @@ public class ParseRelation { } /** - * Parses a relation from JSON. + * Parses a relation from JSON with the given decoder. */ /* package */ ParseRelation(JSONObject jsonObject, ParseDecoder decoder) { this.parent = null; @@ -75,6 +78,21 @@ public class ParseRelation { } } + /** + * Creates a ParseRelation from a Parcel with the given decoder. + */ + /* package */ ParseRelation(Parcel source, ParseParcelableDecoder decoder) { + if (source.readByte() == 1) this.key = source.readString(); + if (source.readByte() == 1) this.targetClass = source.readString(); + if (source.readByte() == 1) this.parentClassName = source.readString(); + if (source.readByte() == 1) this.parentObjectId = source.readString(); + if (source.readByte() == 1) this.parent = new WeakReference<>((ParseObject) decoder.decode(source)); + int size = source.readInt(); + for (int i = 0; i < size; i++) { + knownObjects.add((ParseObject) decoder.decode(source)); + } + } + /* package */ void ensureParentAndKey(ParseObject someParent, String someKey) { synchronized (mutex) { if (parent == null) { @@ -224,4 +242,47 @@ public ParseQuery getQuery() { /* package for tests */ Set getKnownObjects() { return knownObjects; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + writeToParcel(dest, ParseParcelableEncoder.get()); + } + + /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { + synchronized (mutex) { + // Fields are all nullable. + dest.writeByte(key != null ? (byte) 1 : 0); + if (key != null) dest.writeString(key); + dest.writeByte(targetClass != null ? (byte) 1 : 0); + if (targetClass != null) dest.writeString(targetClass); + dest.writeByte(parentClassName != null ? (byte) 1 : 0); + if (parentClassName != null) dest.writeString(parentClassName); + dest.writeByte(parentObjectId != null ? (byte) 1 : 0); + if (parentObjectId != null) dest.writeString(parentObjectId); + boolean has = parent != null && parent.get() != null; + dest.writeByte(has ? (byte) 1 : 0); + if (has) encoder.encode(parent.get(), dest); + dest.writeInt(knownObjects.size()); + for (ParseObject obj : knownObjects) { + encoder.encode(obj, dest); + } + } + } + + public final static Creator CREATOR = new Creator() { + @Override + public ParseRelation createFromParcel(Parcel source) { + return new ParseRelation(source, ParseParcelableDecoder.get()); + } + + @Override + public ParseRelation[] newArray(int size) { + return new ParseRelation[size]; + } + }; } diff --git a/Parse/src/test/java/com/parse/ParseFileTest.java b/Parse/src/test/java/com/parse/ParseFileTest.java index d9776262c..8e44c79a3 100644 --- a/Parse/src/test/java/com/parse/ParseFileTest.java +++ b/Parse/src/test/java/com/parse/ParseFileTest.java @@ -43,8 +43,8 @@ public class ParseFileTest { @Before public void setup() { - ParseCorePlugins.getInstance().reset(); - ParseTestUtils.setTestParseUser(); + ParseCorePlugins.getInstance().reset(); + ParseTestUtils.setTestParseUser(); } @After From e68a2daa8e92851168dce689c461276035bbf7ed Mon Sep 17 00:00:00 2001 From: miav Date: Wed, 5 Apr 2017 22:38:55 +0200 Subject: [PATCH 07/10] Encode circular references as pointers. Support for ParseRelation --- Parse/src/main/java/com/parse/ParseACL.java | 11 +++-- .../java/com/parse/ParseAddOperation.java | 2 +- .../com/parse/ParseAddUniqueOperation.java | 2 +- .../java/com/parse/ParseDeleteOperation.java | 2 +- .../java/com/parse/ParseFieldOperation.java | 24 +++++----- .../com/parse/ParseIncrementOperation.java | 2 +- .../src/main/java/com/parse/ParseObject.java | 43 +++++++++++------- .../com/parse/ParseObjectParcelDecoder.java | 42 ++++++++++++++++++ .../com/parse/ParseObjectParcelEncoder.java | 37 ++++++++++++++++ .../java/com/parse/ParseOperationSet.java | 4 +- ...leDecoder.java => ParseParcelDecoder.java} | 44 ++++++++++++------- ...leEncoder.java => ParseParcelEncoder.java} | 18 +++++--- .../main/java/com/parse/ParseRelation.java | 8 ++-- .../com/parse/ParseRelationOperation.java | 2 +- .../java/com/parse/ParseRemoveOperation.java | 2 +- .../java/com/parse/ParseSetOperation.java | 2 +- Parse/src/main/java/com/parse/ParseUser.java | 8 ++-- .../src/test/java/com/parse/ParseACLTest.java | 7 +-- .../java/com/parse/ParseObjectStateTest.java | 4 +- .../test/java/com/parse/ParseObjectTest.java | 44 +++++++++++++------ .../test/java/com/parse/ParseUserTest.java | 1 + 21 files changed, 220 insertions(+), 89 deletions(-) create mode 100644 Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java create mode 100644 Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java rename Parse/src/main/java/com/parse/{ParseParcelableDecoder.java => ParseParcelDecoder.java} (63%) rename Parse/src/main/java/com/parse/{ParseParcelableEncoder.java => ParseParcelEncoder.java} (89%) diff --git a/Parse/src/main/java/com/parse/ParseACL.java b/Parse/src/main/java/com/parse/ParseACL.java index 32955069d..e95cfedc2 100644 --- a/Parse/src/main/java/com/parse/ParseACL.java +++ b/Parse/src/main/java/com/parse/ParseACL.java @@ -564,10 +564,10 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - writeToParcel(dest, ParseParcelableEncoder.get()); + writeToParcel(dest, new ParseObjectParcelEncoder()); } - /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { dest.writeByte(shared ? (byte) 1 : 0); dest.writeInt(permissionsById.size()); Set keys = permissionsById.keySet(); @@ -578,8 +578,7 @@ public void writeToParcel(Parcel dest, int flags) { } dest.writeByte(unresolvedUser != null ? (byte) 1 : 0); if (unresolvedUser != null) { - // Ensure it has a local Id so we recognize it after parceling - unresolvedUser.getOrCreateLocalId(); + // Encoder will create a local id for unresolvedUser, so we recognize it after unparcel. encoder.encode(unresolvedUser, dest); } } @@ -587,7 +586,7 @@ public void writeToParcel(Parcel dest, int flags) { public final static Creator CREATOR = new Creator() { @Override public ParseACL createFromParcel(Parcel source) { - return new ParseACL(source, ParseParcelableDecoder.get()); + return new ParseACL(source, new ParseObjectParcelDecoder()); } @Override @@ -596,7 +595,7 @@ public ParseACL[] newArray(int size) { } }; - /* package */ ParseACL(Parcel source, ParseParcelableDecoder decoder) { + /* package */ ParseACL(Parcel source, ParseParcelDecoder decoder) { shared = source.readByte() == 1; int size = source.readInt(); for (int i = 0; i < size; i++) { diff --git a/Parse/src/main/java/com/parse/ParseAddOperation.java b/Parse/src/main/java/com/parse/ParseAddOperation.java index 54a226909..fe1959aa2 100644 --- a/Parse/src/main/java/com/parse/ParseAddOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddOperation.java @@ -39,7 +39,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { diff --git a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java index c7a0e85ef..8f19827d9 100644 --- a/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java +++ b/Parse/src/main/java/com/parse/ParseAddUniqueOperation.java @@ -41,7 +41,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { diff --git a/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/Parse/src/main/java/com/parse/ParseDeleteOperation.java index fc38f938b..c817d05bc 100644 --- a/Parse/src/main/java/com/parse/ParseDeleteOperation.java +++ b/Parse/src/main/java/com/parse/ParseDeleteOperation.java @@ -36,7 +36,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); } diff --git a/Parse/src/main/java/com/parse/ParseFieldOperation.java b/Parse/src/main/java/com/parse/ParseFieldOperation.java index 2d254e208..bd33d7139 100644 --- a/Parse/src/main/java/com/parse/ParseFieldOperation.java +++ b/Parse/src/main/java/com/parse/ParseFieldOperation.java @@ -46,7 +46,7 @@ * @param parcelableEncoder * A ParseParcelableEncoder. */ - void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder); + void encode(Parcel dest, ParseParcelEncoder parcelableEncoder); /** * Returns a field operation that is composed of a previous operation followed by this operation. @@ -90,7 +90,7 @@ private ParseFieldOperations() { */ private interface ParseFieldOperationFactory { ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throws JSONException; - ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder); + ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder); } // A map of all known decoders. @@ -122,7 +122,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { // Decode AddRelation and then RemoveRelation ParseFieldOperation add = ParseFieldOperations.decode(source, decoder); ParseFieldOperation remove = ParseFieldOperations.decode(source, decoder); @@ -138,7 +138,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { return ParseDeleteOperation.getInstance(); } }); @@ -151,7 +151,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { return new ParseIncrementOperation((Number) decoder.decode(source)); } }); @@ -164,7 +164,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { int size = source.readInt(); List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { @@ -182,7 +182,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { int size = source.readInt(); List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { @@ -200,7 +200,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { int size = source.readInt(); List list = new ArrayList<>(size); for (int i = 0; i < size; i++) { @@ -220,7 +220,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { int size = source.readInt(); Set set = new HashSet<>(size); for (int i = 0; i < size; i++) { @@ -240,7 +240,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { int size = source.readInt(); Set set = new HashSet<>(size); for (int i = 0; i < size; i++) { @@ -257,7 +257,7 @@ public ParseFieldOperation decode(JSONObject object, ParseDecoder decoder) throw } @Override - public ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + public ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { return new ParseSetOperation(decoder.decode(source)); } }); @@ -289,7 +289,7 @@ static ParseFieldOperation decode(JSONObject encoded, ParseDecoder decoder) thro * * @return A ParseFieldOperation. */ - static ParseFieldOperation decode(Parcel source, ParseParcelableDecoder decoder) { + static ParseFieldOperation decode(Parcel source, ParseParcelDecoder decoder) { String op = source.readString(); ParseFieldOperationFactory factory = opDecoderMap.get(op); if (factory == null) { diff --git a/Parse/src/main/java/com/parse/ParseIncrementOperation.java b/Parse/src/main/java/com/parse/ParseIncrementOperation.java index 73eabd2f8..b2c56c2fa 100644 --- a/Parse/src/main/java/com/parse/ParseIncrementOperation.java +++ b/Parse/src/main/java/com/parse/ParseIncrementOperation.java @@ -34,7 +34,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); parcelableEncoder.encode(amount, dest); // Let encoder figure out how to parcel Number } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 14189c8a3..7de5cf5fb 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -100,12 +100,12 @@ public static Init newBuilder(String className) { return new Builder(className); } - /* package */ static State createFromParcel(Parcel source) { + /* package */ static State createFromParcel(Parcel source, ParseParcelDecoder decoder) { String className = source.readString(); if ("_User".equals(className)) { - return new ParseUser.State(source, className); + return new ParseUser.State(source, className, decoder); } - return new State(source, className); + return new State(source, className, decoder); } /** package */ static abstract class Init { @@ -276,9 +276,8 @@ public State build() { availableKeys = new HashSet<>(builder.availableKeys); } - /* package */ State(Parcel parcel, String clazz) { - ParseParcelableDecoder decoder = ParseParcelableDecoder.get(); - className = clazz; + /* package */ State(Parcel parcel, String clazz, ParseParcelDecoder decoder) { + className = clazz; // Already read objectId = parcel.readByte() == 1 ? parcel.readString() : null; createdAt = parcel.readLong(); long updated = parcel.readLong(); @@ -339,8 +338,7 @@ public Set availableKeys() { return availableKeys; } - protected void writeToParcel(Parcel dest) { - ParseParcelableEncoder encoder = ParseParcelableEncoder.get(); + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { dest.writeString(className); dest.writeByte(objectId != null ? (byte) 1 : 0); if (objectId != null) { @@ -4234,12 +4232,18 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - writeToParcel(dest, ParseParcelableEncoder.get()); + writeToParcel(dest, new ParseObjectParcelEncoder(this)); } - /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { synchronized (mutex) { - state.writeToParcel(dest); + // Write className and id regardless of state. + dest.writeString(getClassName()); + String objectId = getObjectId(); + dest.writeByte(objectId != null ? (byte) 1 : 0); + if (objectId != null) dest.writeString(objectId); + // Write state and other members + state.writeToParcel(dest, encoder); dest.writeByte(localId != null ? (byte) 1 : 0); if (localId != null) dest.writeString(localId); dest.writeByte(isDeleted ? (byte) 1 : 0); @@ -4272,7 +4276,7 @@ public void writeToParcel(Parcel dest, int flags) { public final static Creator CREATOR = new Creator() { @Override public ParseObject createFromParcel(Parcel source) { - return ParseObject.createFromParcel(source, ParseParcelableDecoder.get()); + return ParseObject.createFromParcel(source, new ParseObjectParcelDecoder()); } @Override @@ -4281,9 +4285,18 @@ public ParseObject[] newArray(int size) { } }; - /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelableDecoder decoder) { - State state = State.createFromParcel(source); // Returns ParseUser.State if needed - ParseObject obj = create(state.className); // Returns the correct subclass + /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) { + ParseObject obj; + String className = source.readString(); + if (source.readByte() == 1) { // We have an objectId. + obj = createWithoutData(className, source.readString()); + } else { + obj = create(className); + } + if (decoder instanceof ParseObjectParcelDecoder) { + ((ParseObjectParcelDecoder) decoder).addKnownObject(obj); + } + State state = State.createFromParcel(source, decoder); // Returns ParseUser.State if needed obj.setState(state); // This calls rebuildEstimatedData if (source.readByte() == 1) obj.localId = source.readString(); if (source.readByte() == 1) obj.isDeleted = true; diff --git a/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java new file mode 100644 index 000000000..e75bcc956 --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java @@ -0,0 +1,42 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelDecoder} that remembers which + * {@code ParseObject}s have been decoded. When a pointer is found and we have already decoded + * an instance for the same object id, we use the decoded instance. + * + * This is very similar to what {@link KnownParseObjectDecoder} does for JSON, and is meant to be + * used with {@link ParseObjectParcelEncoder}. + */ +/* package */ class ParseObjectParcelDecoder extends ParseParcelDecoder { + + private Map objects = new HashMap<>(); + + public ParseObjectParcelDecoder() {} + + public void addKnownObject(ParseObject object) { + objects.put(getObjectOrLocalId(object), object); + } + + @Override + protected ParseObject decodePointer(Parcel source) { + String className = source.readString(); + String objectId = source.readString(); + if (objects.containsKey(objectId)) { + return objects.get(objectId); + } + // Should not happen if using in conjunction with ParseObjectParcelEncoder . + return ParseObject.createWithoutData(className, objectId); + } + + /* package for tests */ String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java b/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java new file mode 100644 index 000000000..f30e6de86 --- /dev/null +++ b/Parse/src/main/java/com/parse/ParseObjectParcelEncoder.java @@ -0,0 +1,37 @@ +package com.parse; + +import android.os.Parcel; + +import java.util.HashSet; +import java.util.Set; + +/** + * This is a stateful implementation of {@link ParseParcelEncoder} that remembers which + * {@code ParseObject}s have been encoded. If an object is found again in the object tree, + * it is encoded as a pointer rather than a full object, to avoid {@code StackOverflowError}s + * due to circular references. + */ +/* package */ class ParseObjectParcelEncoder extends ParseParcelEncoder { + + private Set ids = new HashSet<>(); + + public ParseObjectParcelEncoder() {} + + public ParseObjectParcelEncoder(ParseObject root) { + ids.add(getObjectOrLocalId(root)); + } + + @Override + protected void encodeParseObject(ParseObject object, Parcel dest) { + String id = getObjectOrLocalId(object); + if (ids.contains(id)) { + encodePointer(object.getClassName(), id, dest); + } else { + super.encodeParseObject(object, dest); + } + } + + private String getObjectOrLocalId(ParseObject object) { + return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); + } +} diff --git a/Parse/src/main/java/com/parse/ParseOperationSet.java b/Parse/src/main/java/com/parse/ParseOperationSet.java index f34753c74..1cddc29f4 100644 --- a/Parse/src/main/java/com/parse/ParseOperationSet.java +++ b/Parse/src/main/java/com/parse/ParseOperationSet.java @@ -145,7 +145,7 @@ public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) /** * Parcels this operation set into a Parcel with the given encoder. */ - /* package */ void toParcel(Parcel dest, ParseParcelableEncoder encoder) { + /* package */ void toParcel(Parcel dest, ParseParcelEncoder encoder) { dest.writeString(uuid); dest.writeByte(isSaveEventually ? (byte) 1 : 0); dest.writeInt(size()); @@ -155,7 +155,7 @@ public static ParseOperationSet fromRest(JSONObject json, ParseDecoder decoder) } } - /* package */ static ParseOperationSet fromParcel(Parcel source, ParseParcelableDecoder decoder) { + /* package */ static ParseOperationSet fromParcel(Parcel source, ParseParcelDecoder decoder) { ParseOperationSet set = new ParseOperationSet(source.readString()); set.setIsSaveEventually(source.readByte() == 1); int size = source.readInt(); diff --git a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java b/Parse/src/main/java/com/parse/ParseParcelDecoder.java similarity index 63% rename from Parse/src/main/java/com/parse/ParseParcelableDecoder.java rename to Parse/src/main/java/com/parse/ParseParcelDecoder.java index bfcac4707..09f65c1e7 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableDecoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelDecoder.java @@ -21,14 +21,14 @@ * A {@code ParseParcelableDecoder} can be used to unparcel objects such as * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. * - * @see com.parse.ParseParcelableEncoder + * @see ParseParcelEncoder */ -/* package */ class ParseParcelableDecoder { +/* package */ class ParseParcelDecoder { // This class isn't really a Singleton, but since it has no state, it's more efficient to get the // default instance. - private static final ParseParcelableDecoder INSTANCE = new ParseParcelableDecoder(); - public static ParseParcelableDecoder get() { + private static final ParseParcelDecoder INSTANCE = new ParseParcelDecoder(); + public static ParseParcelDecoder get() { return INSTANCE; } @@ -36,28 +36,31 @@ public Object decode(Parcel source) { String type = source.readString(); switch (type) { - case ParseParcelableEncoder.TYPE_OBJECT: - return ParseObject.createFromParcel(source, this); + case ParseParcelEncoder.TYPE_OBJECT: + return decodeParseObject(source); - case ParseParcelableEncoder.TYPE_DATE: + case ParseParcelEncoder.TYPE_POINTER: + return decodePointer(source); + + case ParseParcelEncoder.TYPE_DATE: String iso = source.readString(); return ParseDateFormat.getInstance().parse(iso); - case ParseParcelableEncoder.TYPE_BYTES: + case ParseParcelEncoder.TYPE_BYTES: byte[] bytes = new byte[source.readInt()]; source.readByteArray(bytes); return bytes; - case ParseParcelableEncoder.TYPE_OP: + case ParseParcelEncoder.TYPE_OP: return ParseFieldOperations.decode(source, this); - case ParseParcelableEncoder.TYPE_ACL: + case ParseParcelEncoder.TYPE_ACL: return new ParseACL(source, this); - case ParseParcelableEncoder.TYPE_RELATION: + case ParseParcelEncoder.TYPE_RELATION: return new ParseRelation<>(source, this); - case ParseParcelableEncoder.TYPE_MAP: + case ParseParcelEncoder.TYPE_MAP: int size = source.readInt(); Map map = new HashMap<>(size); for (int i = 0; i < size; i++) { @@ -65,7 +68,7 @@ public Object decode(Parcel source) { } return map; - case ParseParcelableEncoder.TYPE_COLLECTION: + case ParseParcelEncoder.TYPE_COLLECTION: int length = source.readInt(); List list = new ArrayList<>(length); for (int i = 0; i < length; i++) { @@ -73,13 +76,13 @@ public Object decode(Parcel source) { } return list; - case ParseParcelableEncoder.TYPE_JSON_NULL: + case ParseParcelEncoder.TYPE_JSON_NULL: return JSONObject.NULL; - case ParseParcelableEncoder.TYPE_NULL: + case ParseParcelEncoder.TYPE_NULL: return null; - case ParseParcelableEncoder.TYPE_NATIVE: + case ParseParcelEncoder.TYPE_NATIVE: return source.readValue(null); // No need for a class loader. default: @@ -88,4 +91,13 @@ public Object decode(Parcel source) { } } + protected ParseObject decodeParseObject(Parcel source) { + return ParseObject.createFromParcel(source, this); + } + + protected ParseObject decodePointer(Parcel source) { + // By default, use createWithoutData. Overriden in subclass. + return ParseObject.createWithoutData(source.readString(), source.readString()); + } + } diff --git a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java b/Parse/src/main/java/com/parse/ParseParcelEncoder.java similarity index 89% rename from Parse/src/main/java/com/parse/ParseParcelableEncoder.java rename to Parse/src/main/java/com/parse/ParseParcelEncoder.java index 361d9ad95..840026880 100644 --- a/Parse/src/main/java/com/parse/ParseParcelableEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -21,14 +21,14 @@ * A {@code ParseParcelableEncoder} can be used to parcel objects such as * {@link com.parse.ParseObject} into a {@link android.os.Parcel}. * - * @see com.parse.ParseParcelableDecoder + * @see ParseParcelDecoder */ -/* package */ class ParseParcelableEncoder { +/* package */ class ParseParcelEncoder { // This class isn't really a Singleton, but since it has no state, it's more efficient to get the // default instance. - private static final ParseParcelableEncoder INSTANCE = new ParseParcelableEncoder(); - public static ParseParcelableEncoder get() { + private static final ParseParcelEncoder INSTANCE = new ParseParcelEncoder(); + public static ParseParcelEncoder get() { return INSTANCE; } @@ -50,6 +50,7 @@ public static ParseParcelableEncoder get() { } /* package */ final static String TYPE_OBJECT = "ParseObject"; + /* package */ final static String TYPE_POINTER = "Pointer"; /* package */ final static String TYPE_DATE = "Date"; /* package */ final static String TYPE_BYTES = "Bytes"; /* package */ final static String TYPE_ACL = "Acl"; @@ -64,7 +65,7 @@ public static ParseParcelableEncoder get() { public void encode(Object object, Parcel dest) { try { if (object instanceof ParseObject) { - dest.writeString(TYPE_OBJECT); + // By default, encode as a full ParseObject. Overriden in sublasses. encodeParseObject((ParseObject) object, dest); } else if (object instanceof Date) { @@ -139,6 +140,13 @@ public void encode(Object object, Parcel dest) { } protected void encodeParseObject(ParseObject object, Parcel dest) { + dest.writeString(TYPE_OBJECT); object.writeToParcel(dest, this); } + + protected void encodePointer(String className, String objectId, Parcel dest) { + dest.writeString(TYPE_POINTER); + dest.writeString(className); + dest.writeString(objectId); + } } diff --git a/Parse/src/main/java/com/parse/ParseRelation.java b/Parse/src/main/java/com/parse/ParseRelation.java index a54d69fa5..4a6a1720b 100644 --- a/Parse/src/main/java/com/parse/ParseRelation.java +++ b/Parse/src/main/java/com/parse/ParseRelation.java @@ -81,7 +81,7 @@ public class ParseRelation implements Parcelable { /** * Creates a ParseRelation from a Parcel with the given decoder. */ - /* package */ ParseRelation(Parcel source, ParseParcelableDecoder decoder) { + /* package */ ParseRelation(Parcel source, ParseParcelDecoder decoder) { if (source.readByte() == 1) this.key = source.readString(); if (source.readByte() == 1) this.targetClass = source.readString(); if (source.readByte() == 1) this.parentClassName = source.readString(); @@ -250,10 +250,10 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { - writeToParcel(dest, ParseParcelableEncoder.get()); + writeToParcel(dest, new ParseObjectParcelEncoder()); } - /* package */ void writeToParcel(Parcel dest, ParseParcelableEncoder encoder) { + /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { synchronized (mutex) { // Fields are all nullable. dest.writeByte(key != null ? (byte) 1 : 0); @@ -277,7 +277,7 @@ public void writeToParcel(Parcel dest, int flags) { public final static Creator CREATOR = new Creator() { @Override public ParseRelation createFromParcel(Parcel source) { - return new ParseRelation(source, ParseParcelableDecoder.get()); + return new ParseRelation(source, new ParseObjectParcelDecoder()); } @Override diff --git a/Parse/src/main/java/com/parse/ParseRelationOperation.java b/Parse/src/main/java/com/parse/ParseRelationOperation.java index d26122761..a3075f30d 100644 --- a/Parse/src/main/java/com/parse/ParseRelationOperation.java +++ b/Parse/src/main/java/com/parse/ParseRelationOperation.java @@ -193,7 +193,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { if (relationsToAdd.isEmpty() && relationsToRemove.isEmpty()) { throw new IllegalArgumentException("A ParseRelationOperation was created without any data."); } diff --git a/Parse/src/main/java/com/parse/ParseRemoveOperation.java b/Parse/src/main/java/com/parse/ParseRemoveOperation.java index 89b660ec4..0f24bd8c8 100644 --- a/Parse/src/main/java/com/parse/ParseRemoveOperation.java +++ b/Parse/src/main/java/com/parse/ParseRemoveOperation.java @@ -41,7 +41,7 @@ public JSONObject encode(ParseEncoder objectEncoder) throws JSONException { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); dest.writeInt(objects.size()); for (Object object : objects) { diff --git a/Parse/src/main/java/com/parse/ParseSetOperation.java b/Parse/src/main/java/com/parse/ParseSetOperation.java index fc5bf7907..b122af4ac 100644 --- a/Parse/src/main/java/com/parse/ParseSetOperation.java +++ b/Parse/src/main/java/com/parse/ParseSetOperation.java @@ -33,7 +33,7 @@ public Object encode(ParseEncoder objectEncoder) { } @Override - public void encode(Parcel dest, ParseParcelableEncoder parcelableEncoder) { + public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); parcelableEncoder.encode(value, dest); } diff --git a/Parse/src/main/java/com/parse/ParseUser.java b/Parse/src/main/java/com/parse/ParseUser.java index 9dc8f9d0e..4324d6098 100644 --- a/Parse/src/main/java/com/parse/ParseUser.java +++ b/Parse/src/main/java/com/parse/ParseUser.java @@ -129,8 +129,8 @@ private State(Builder builder) { isNew = builder.isNew; } - /* package */ State(Parcel source, String className) { - super(source, className); + /* package */ State(Parcel source, String className, ParseParcelDecoder decoder) { + super(source, className, decoder); isNew = source.readByte() == 1; } @@ -162,8 +162,8 @@ public boolean isNew() { } @Override - protected void writeToParcel(Parcel dest) { - super.writeToParcel(dest); + protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { + super.writeToParcel(dest, encoder); dest.writeByte(isNew ? (byte) 1 : 0); } } diff --git a/Parse/src/test/java/com/parse/ParseACLTest.java b/Parse/src/test/java/com/parse/ParseACLTest.java index dde0d931d..c523b62e3 100644 --- a/Parse/src/test/java/com/parse/ParseACLTest.java +++ b/Parse/src/test/java/com/parse/ParseACLTest.java @@ -199,13 +199,14 @@ public void testParcelableWithUnresolvedUser() throws Exception { setLazy(unresolved); acl.setReadAccess(unresolved, true); - // unresolved users need a local id when parcelling. + // unresolved users need a local id when parcelling and unparcelling. // Since we don't have an Android environment, local id creation will fail. - unresolved.localId = "local_12hs2"; + unresolved.localId = "localId"; Parcel parcel = Parcel.obtain(); acl.writeToParcel(parcel, 0); parcel.setDataPosition(0); - acl = ParseACL.CREATOR.createFromParcel(parcel); + // Do not user ParseObjectParcelDecoder because it requires local ids + acl = new ParseACL(parcel, new ParseParcelDecoder()); assertTrue(acl.getReadAccess(unresolved)); } diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index ced5b4114..b4c9c2ccc 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -102,9 +102,9 @@ public void testParcelable() { .build(); Parcel parcel = Parcel.obtain(); - state.writeToParcel(parcel); + state.writeToParcel(parcel, ParseParcelEncoder.get()); parcel.setDataPosition(0); - ParseObject.State copy = ParseObject.State.createFromParcel(parcel); + ParseObject.State copy = ParseObject.State.createFromParcel(parcel, ParseParcelDecoder.get()); assertEquals(state.className(), copy.className()); assertEquals(state.objectId(), copy.objectId()); diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 1fe474abf..92058c122 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -510,27 +510,36 @@ public void testGetLongWithWrongValue() throws Exception { @Test public void testParcelable() throws Exception { ParseFieldOperations.registerDefaultDecoders(); - ParseObject object = new ParseObject("Test"); + ParseObject object = ParseObject.createWithoutData("Test", "objectId"); object.isDeleted = true; object.put("long", 200L); object.put("double", 30D); object.put("int", 50); object.put("string", "test"); + object.put("date", new Date(200)); + object.put("null", JSONObject.NULL); + // Collection object.put("collection", Arrays.asList("test1", "test2")); - ParseObject other = new ParseObject("Test"); - other.setObjectId("otherId"); + // Pointer + ParseObject other = ParseObject.createWithoutData("Test", "otherId"); object.put("pointer", other); + // Map Map map = new HashMap<>(); map.put("key1", "value"); map.put("key2", 50); object.put("map", map); - object.put("date", new Date(200)); + // Bytes byte[] bytes = new byte[2]; object.put("bytes", bytes); - object.put("null", JSONObject.NULL); + // ACL ParseACL acl = new ParseACL(); acl.setReadAccess("reader", true); object.setACL(acl); + // Relation + ParseObject related = ParseObject.createWithoutData("RelatedClass", "relatedId"); + ParseRelation rel = new ParseRelation<>(object, "relation"); + rel.add(related); + object.put("relation", rel); Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); @@ -538,33 +547,42 @@ public void testParcelable() throws Exception { ParseObject newObject = ParseObject.CREATOR.createFromParcel(parcel); assertEquals(newObject.getClassName(), object.getClassName()); - assertEquals(newObject.isDeleted, true); - assertEquals(newObject.hasChanges(), true); + assertEquals(newObject.isDeleted, object.isDeleted); + assertEquals(newObject.hasChanges(), object.hasChanges()); assertEquals(newObject.getLong("long"), object.getLong("long")); assertEquals(newObject.getDouble("double"), object.getDouble("double"), 0); assertEquals(newObject.getInt("int"), object.getInt("int")); assertEquals(newObject.getString("string"), object.getString("string")); + assertEquals(newObject.getDate("date"), object.getDate("date")); + assertEquals(newObject.get("null"), object.get("null")); assertEquals(newObject.getList("collection"), object.getList("collection")); assertEquals(newObject.getParseObject("pointer").getClassName(), other.getClassName()); assertEquals(newObject.getParseObject("pointer").getObjectId(), other.getObjectId()); assertEquals(newObject.getMap("map"), object.getMap("map")); - assertEquals(newObject.getDate("date"), object.getDate("date")); assertEquals(newObject.getBytes("bytes").length, bytes.length); - assertEquals(newObject.get("null"), object.get("null")); assertEquals(newObject.getACL().getReadAccess("reader"), acl.getReadAccess("reader")); + ParseRelation newRel = newObject.getRelation("relation"); + assertEquals(newRel.getKey(), rel.getKey()); + assertEquals(newRel.getKnownObjects().size(), rel.getKnownObjects().size()); + newRel.hasKnownObject(related); } @Test public void testRecursiveParcel() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); ParseObject object = new ParseObject("Test"); - object.put("itself", object); + object.setObjectId("id"); + object.put("self", object); Parcel parcel = Parcel.obtain(); - // object.writeToParcel(parcel, 0); - // TODO fix this + object.writeToParcel(parcel, new ParseObjectParcelEncoder(object)); + parcel.setDataPosition(0); + ParseObject newObject = ParseObject.createFromParcel(parcel, new ParseObjectParcelDecoder()); + assertEquals(newObject.getObjectId(), "id"); + assertEquals(newObject.getParseObject("self").getObjectId(), "id"); + assertEquals(newObject.getParseObject("self").getParseObject("self").getObjectId(), "id"); } // TODO test ParseGeoPoint and ParseFile after merge - // TODO test subclassing //endregion diff --git a/Parse/src/test/java/com/parse/ParseUserTest.java b/Parse/src/test/java/com/parse/ParseUserTest.java index 26e4176ce..8a275b408 100644 --- a/Parse/src/test/java/com/parse/ParseUserTest.java +++ b/Parse/src/test/java/com/parse/ParseUserTest.java @@ -102,6 +102,7 @@ public void testImmutableKeys() { @Test public void testOnSaveRestoreState() throws Exception { ParseUser user = new ParseUser(); + user.setObjectId("objId"); user.setIsCurrentUser(true); Parcel parcel = Parcel.obtain(); From bf55908b0f0b0edbead9bf64c8f03075101135c3 Mon Sep 17 00:00:00 2001 From: miav Date: Thu, 6 Apr 2017 01:08:15 +0200 Subject: [PATCH 08/10] Test parcel while saving, tests for ParseRelation --- .../com/parse/ParseObjectParcelDecoder.java | 11 ++-- .../java/com/parse/ParseParcelEncoder.java | 2 +- .../test/java/com/parse/ParseObjectTest.java | 56 +++++++++++++++++++ .../java/com/parse/ParseRelationTest.java | 37 ++++++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java index e75bcc956..772c47c9d 100644 --- a/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java +++ b/Parse/src/main/java/com/parse/ParseObjectParcelDecoder.java @@ -12,8 +12,7 @@ * {@code ParseObject}s have been decoded. When a pointer is found and we have already decoded * an instance for the same object id, we use the decoded instance. * - * This is very similar to what {@link KnownParseObjectDecoder} does for JSON, and is meant to be - * used with {@link ParseObjectParcelEncoder}. + * This is very similar to what {@link KnownParseObjectDecoder} does for JSON. */ /* package */ class ParseObjectParcelDecoder extends ParseParcelDecoder { @@ -32,11 +31,13 @@ protected ParseObject decodePointer(Parcel source) { if (objects.containsKey(objectId)) { return objects.get(objectId); } - // Should not happen if using in conjunction with ParseObjectParcelEncoder . - return ParseObject.createWithoutData(className, objectId); + // Should not happen if encoding was done through ParseObjectParcelEncoder. + ParseObject object = ParseObject.createWithoutData(className, objectId); + objects.put(objectId, object); + return object; } - /* package for tests */ String getObjectOrLocalId(ParseObject object) { + private String getObjectOrLocalId(ParseObject object) { return object.getObjectId() != null ? object.getObjectId() : object.getOrCreateLocalId(); } } diff --git a/Parse/src/main/java/com/parse/ParseParcelEncoder.java b/Parse/src/main/java/com/parse/ParseParcelEncoder.java index 840026880..1ca906fb2 100644 --- a/Parse/src/main/java/com/parse/ParseParcelEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -133,7 +133,7 @@ public void encode(Object object, Parcel dest) { + object.getClass().toString()); } - } catch (Exception e) { + } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Could not encode this object into Parcel. " + object.getClass().toString()); } diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 92058c122..d1ef393d1 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -41,6 +41,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) @@ -582,6 +583,61 @@ public void testRecursiveParcel() throws Exception { assertEquals(newObject.getParseObject("self").getParseObject("self").getObjectId(), "id"); } + @Test + public void testParcelWhileSaving() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); + ParseObject object, other; + List> tasks = new ArrayList<>(); + + // Mocked to let save work + ParseCurrentUserController userController = mock(ParseCurrentUserController.class); + when(userController.getAsync()).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(userController); + + // Mocked to simulate in-flight save + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.saveAsync( + any(ParseObject.State.class), + any(ParseOperationSet.class), + anyString(), + any(ParseDecoder.class))) + .thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + + // Create multiple ParseOperationSets + object = new ParseObject("TestObject"); + object.setObjectId("id"); + object.put("key", "value"); + object.put("number", 5); + tasks.add(object.saveInBackground()); + + object.put("key", "newValue"); + object.increment("number", 6); + tasks.add(object.saveInBackground()); + + object.increment("number", -1); + tasks.add(object.saveInBackground()); + + // Ensure Log.w is called... + assertTrue(object.hasOutstandingOperations()); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + other = ParseObject.CREATOR.createFromParcel(parcel); + assertTrue(other.isDirty("key")); + assertTrue(other.isDirty("number")); + assertEquals(other.getString("key"), "newValue"); + assertEquals(other.getNumber("number"), 10); + // By design, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.hasOutstandingOperations()); + + // Force finish save operations on the old instance. + tcs.setResult(null); + ParseTaskUtils.wait(Task.whenAll(tasks)); + } + // TODO test ParseGeoPoint and ParseFile after merge //endregion diff --git a/Parse/src/test/java/com/parse/ParseRelationTest.java b/Parse/src/test/java/com/parse/ParseRelationTest.java index 2cb4c134d..3446a10c6 100644 --- a/Parse/src/test/java/com/parse/ParseRelationTest.java +++ b/Parse/src/test/java/com/parse/ParseRelationTest.java @@ -8,11 +8,16 @@ */ package com.parse; +import android.os.Parcel; + import org.json.JSONArray; import org.json.JSONObject; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.skyscreamer.jsonassert.JSONCompareMode; import static org.junit.Assert.assertEquals; @@ -24,6 +29,8 @@ import static org.mockito.Mockito.when; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) public class ParseRelationTest { @Rule @@ -77,6 +84,36 @@ public void testConstructorWithJSONAndDecoder() throws Exception { //endregion + //region testParcelable + + @Test + public void testParcelable() throws Exception { + ParseFieldOperations.registerDefaultDecoders(); + ParseRelation relation = new ParseRelation<>("Test"); + ParseObject parent = new ParseObject("Parent"); + parent.setObjectId("parentId"); + relation.ensureParentAndKey(parent, "key"); + ParseObject inner = new ParseObject("Test"); + inner.setObjectId("innerId"); + relation.add(inner); + + Parcel parcel = Parcel.obtain(); + relation.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + //noinspection unchecked + ParseRelation newRelation = ParseRelation.CREATOR.createFromParcel(parcel); + assertEquals(newRelation.getTargetClass(), "Test"); + assertEquals(newRelation.getKey(), "key"); + assertEquals(newRelation.getParent().getClassName(), "Parent"); + assertEquals(newRelation.getParent().getObjectId(), "parentId"); + assertEquals(newRelation.getKnownObjects().size(), 1); + + // This would fail assertTrue(newRelation.hasKnownObject(inner)). + // That is because ParseRelation uses == to check for known objects. + } + + //endregion + //region testEnsureParentAndKey @Test From 9e1240d367fd3bfd742bb658cfb7f8abf3a09cbe Mon Sep 17 00:00:00 2001 From: miav Date: Thu, 6 Apr 2017 17:48:35 +0200 Subject: [PATCH 09/10] Warn with delete operations ongoing. Support for LDS --- .../java/com/parse/ParseDeleteOperation.java | 2 +- .../src/main/java/com/parse/ParseObject.java | 88 ++++++--- .../java/com/parse/ParseParcelEncoder.java | 27 +-- .../test/java/com/parse/ParseObjectTest.java | 181 +++++++++++++----- 4 files changed, 195 insertions(+), 103 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseDeleteOperation.java b/Parse/src/main/java/com/parse/ParseDeleteOperation.java index c817d05bc..fb5393ac0 100644 --- a/Parse/src/main/java/com/parse/ParseDeleteOperation.java +++ b/Parse/src/main/java/com/parse/ParseDeleteOperation.java @@ -40,7 +40,7 @@ public void encode(Parcel dest, ParseParcelEncoder parcelableEncoder) { dest.writeString(OP_NAME); } - @Override + @Override public ParseFieldOperation mergeWithPrevious(ParseFieldOperation previous) { return this; } diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 7de5cf5fb..18cf94df3 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -386,8 +386,10 @@ public String toString() { private final ParseMulticastDelegate saveEvent = new ParseMulticastDelegate<>(); /* package */ boolean isDeleted; + /* package */ boolean isDeleting; // Since delete ops are queued, we don't need a counter. //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. /* package */ int isDeletingEventually; + private boolean ldsEnabledWhenParceling; private static final ThreadLocal isCreatingPointerForObjectId = new ThreadLocal() { @@ -2158,6 +2160,7 @@ private Task deleteAsync(final String sessionToken, Task toAwait) { return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { + isDeleting = true; if (state.objectId() == null) { return task.cast(); // no reason to call delete since it doesn't exist } @@ -2168,6 +2171,12 @@ public Task then(Task task) throws Exception { public Task then(Task task) throws Exception { return handleDeleteResultAsync(); } + }).continueWith(new Continuation() { + @Override + public Void then(Task task) throws Exception { + isDeleting = false; + return null; + } }); } @@ -4237,7 +4246,26 @@ public void writeToParcel(Parcel dest, int flags) { /* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) { synchronized (mutex) { - // Write className and id regardless of state. + // Developer warnings. + ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled(); + boolean saving = hasOutstandingOperations(); + boolean deleting = isDeleting || isDeletingEventually > 0; + if (saving) { + Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will act as if these tasks have failed. This means that " + + "the subsequent call to save() will update again the same keys, and this is dangerous " + + "for certain operations, like increment(). To avoid inconsistencies, wait for operations " + + "to end before parceling."); + } + if (deleting) { + Log.w(TAG, "About to parcel a ParseObject while a delete / deleteEventually operation is " + + "going on. If recovered from LDS, the unparceled object will be internally updated when " + + "these tasks end. If not, it will assume it's not deleted, and might incorrectly " + + "return false for isDirty(). To avoid inconsistencies, wait for operations to end " + + "before parceling."); + } + // Write className and id first, regardless of state. dest.writeString(getClassName()); String objectId = getObjectId(); dest.writeByte(objectId != null ? (byte) 1 : 0); @@ -4247,26 +4275,23 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte(localId != null ? (byte) 1 : 0); if (localId != null) dest.writeString(localId); dest.writeByte(isDeleted ? (byte) 1 : 0); - // Squash the operations queue if needed. + // Care about dirty changes and ongoing tasks. ParseOperationSet set; - if (hasOutstandingOperations()) { // There's more than one set. - Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " + - "going on. The unparceled object will act as if these tasks had failed. This " + - "means that the subsequent call to save() will update again the same keys. " + - "This is dangerous for certain operations, like increment() and decrement(). " + - "To avoid inconsistencies, wait for save operations to end before parceling."); - ListIterator iterator = operationSetQueue.listIterator(); + if (hasOutstandingOperations()) { + // There's more than one set. Squash the queue, creating copies + // to preserve the original queue when LDS is enabled. set = new ParseOperationSet(); - while (iterator.hasNext()) { - ParseOperationSet other = iterator.next(); - other.mergeFrom(set); - set = other; + for (ParseOperationSet operationSet : operationSetQueue) { + ParseOperationSet copy = new ParseOperationSet(operationSet); + copy.mergeFrom(set); + set = copy; } } else { set = operationSetQueue.getLast(); } set.setIsSaveEventually(false); set.toParcel(dest, encoder); + // Pass a Bundle to subclasses. Bundle bundle = new Bundle(); onSaveInstanceState(bundle); dest.writeBundle(bundle); @@ -4286,28 +4311,31 @@ public ParseObject[] newArray(int size) { }; /* package */ static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) { - ParseObject obj; String className = source.readString(); - if (source.readByte() == 1) { // We have an objectId. - obj = createWithoutData(className, source.readString()); - } else { - obj = create(className); - } + String objectId = source.readByte() == 1 ? source.readString() : null; + // Create empty object (might be the same instance if LDS is enabled) + // and pass to decoder before unparceling child objects in State + ParseObject object = createWithoutData(className, objectId); if (decoder instanceof ParseObjectParcelDecoder) { - ((ParseObjectParcelDecoder) decoder).addKnownObject(obj); - } - State state = State.createFromParcel(source, decoder); // Returns ParseUser.State if needed - obj.setState(state); // This calls rebuildEstimatedData - if (source.readByte() == 1) obj.localId = source.readString(); - if (source.readByte() == 1) obj.isDeleted = true; + ((ParseObjectParcelDecoder) decoder).addKnownObject(object); + } + State state = State.createFromParcel(source, decoder); + object.setState(state); + if (source.readByte() == 1) object.localId = source.readString(); + if (source.readByte() == 1) object.isDeleted = true; + // If object.ldsEnabledWhenParceling is true, we got this from OfflineStore. + // There is no need to restore operations in that case. + boolean restoreOperations = !object.ldsEnabledWhenParceling; ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder); - for (String key : set.keySet()) { - ParseFieldOperation op = set.get(key); - obj.performOperation(key, op); // Update ops and estimatedData + if (restoreOperations) { + for (String key : set.keySet()) { + ParseFieldOperation op = set.get(key); + object.performOperation(key, op); // Update ops and estimatedData + } } Bundle bundle = source.readBundle(ParseObject.class.getClassLoader()); - obj.onRestoreInstanceState(bundle); - return obj; + object.onRestoreInstanceState(bundle); + return object; } /** diff --git a/Parse/src/main/java/com/parse/ParseParcelEncoder.java b/Parse/src/main/java/com/parse/ParseParcelEncoder.java index 1ca906fb2..9eb0c04ed 100644 --- a/Parse/src/main/java/com/parse/ParseParcelEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -32,21 +32,9 @@ public static ParseParcelEncoder get() { return INSTANCE; } - // TODO: remove this and user ParseEncoder.isValidType. - /* package */ static boolean isValidType(Object value) { - return value instanceof String - || value instanceof Number - || value instanceof Boolean - || value instanceof Date - || value instanceof List - || value instanceof Map - || value instanceof byte[] - || value == JSONObject.NULL - || value instanceof ParseObject - || value instanceof ParseACL - // TODO: waiting merge || value instanceof ParseFile - // TODO: waiting merge || value instanceof ParseGeoPoint - || value instanceof ParseRelation; + private static boolean isValidType(Object value) { + // This encodes to parcel what ParseEncoder does for JSON + return ParseEncoder.isValidType(value); } /* package */ final static String TYPE_OBJECT = "ParseObject"; @@ -82,10 +70,10 @@ public void encode(Object object, Parcel dest) { dest.writeString(TYPE_OP); ((ParseFieldOperation) object).encode(dest, this); - } else if (object instanceof ParseFile) { // TODO + } else if (object instanceof ParseFile) { throw new IllegalArgumentException("Not supported yet"); - } else if (object instanceof ParseGeoPoint) { // TODO + } else if (object instanceof ParseGeoPoint) { throw new IllegalArgumentException("Not supported yet"); } else if (object instanceof ParseACL) { @@ -114,9 +102,6 @@ public void encode(Object object, Parcel dest) { encode(item, dest); } - } else if (object instanceof ParseRelation) {// TODO - throw new IllegalArgumentException("Not supported yet."); - } else if (object == JSONObject.NULL) { dest.writeString(TYPE_JSON_NULL); @@ -133,7 +118,7 @@ public void encode(Object object, Parcel dest) { + object.getClass().toString()); } - } catch (IllegalArgumentException e) { + } catch (Exception e) { throw new IllegalArgumentException("Could not encode this object into Parcel. " + object.getClass().toString()); } diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index d1ef393d1..c56109c8d 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -14,6 +14,7 @@ import org.json.JSONException; import org.json.JSONObject; import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -37,8 +38,11 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -51,6 +55,11 @@ public class ParseObjectTest { @Rule public ExpectedException thrown = ExpectedException.none(); + @Before + public void setUp() { + ParseFieldOperations.registerDefaultDecoders(); // to test JSON / Parcel decoding + } + @After public void tearDown() { ParseCorePlugins.getInstance().reset(); @@ -71,8 +80,6 @@ public void testFromJSONPayload() throws JSONException { "\"age\":33" + "}"); - ParseFieldOperations.registerDefaultDecoders(); - ParseObject parseObject = ParseObject.fromJSONPayload(json, ParseDecoder.get()); assertEquals("GameScore", parseObject.getClassName()); assertEquals("TT1ZskATqS", parseObject.getObjectId()); @@ -101,20 +108,10 @@ public void testRevert() throws ParseException { List> tasks = new ArrayList<>(); // Mocked to let save work - ParseCurrentUserController userController = mock(ParseCurrentUserController.class); - when(userController.getAsync()).thenReturn(Task.forResult(null)); - ParseCorePlugins.getInstance().registerCurrentUserController(userController); + mockCurrentUserController(); // Mocked to simulate in-flight save - TaskCompletionSource tcs = new TaskCompletionSource(); - ParseObjectController objectController = mock(ParseObjectController.class); - when(objectController.saveAsync( - any(ParseObject.State.class), - any(ParseOperationSet.class), - anyString(), - any(ParseDecoder.class))) - .thenReturn(tcs.getTask()); - ParseCorePlugins.getInstance().registerObjectController(objectController); + TaskCompletionSource tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); @@ -173,20 +170,10 @@ public void testRevertKey() throws ParseException { List> tasks = new ArrayList<>(); // Mocked to let save work - ParseCurrentUserController userController = mock(ParseCurrentUserController.class); - when(userController.getAsync()).thenReturn(Task.forResult(null)); - ParseCorePlugins.getInstance().registerCurrentUserController(userController); + mockCurrentUserController(); // Mocked to simulate in-flight save - TaskCompletionSource tcs = new TaskCompletionSource(); - ParseObjectController objectController = mock(ParseObjectController.class); - when(objectController.saveAsync( - any(ParseObject.State.class), - any(ParseOperationSet.class), - anyString(), - any(ParseDecoder.class))) - .thenReturn(tcs.getTask()); - ParseCorePlugins.getInstance().registerObjectController(objectController); + TaskCompletionSource tcs = mockObjectControllerForSave(); // New clean object ParseObject object = new ParseObject("TestObject"); @@ -510,7 +497,7 @@ public void testGetLongWithWrongValue() throws Exception { @Test public void testParcelable() throws Exception { - ParseFieldOperations.registerDefaultDecoders(); + // TODO test ParseGeoPoint and ParseFile after merge ParseObject object = ParseObject.createWithoutData("Test", "objectId"); object.isDeleted = true; object.put("long", 200L); @@ -570,7 +557,6 @@ public void testParcelable() throws Exception { @Test public void testRecursiveParcel() throws Exception { - ParseFieldOperations.registerDefaultDecoders(); ParseObject object = new ParseObject("Test"); object.setObjectId("id"); object.put("self", object); @@ -585,28 +571,12 @@ public void testRecursiveParcel() throws Exception { @Test public void testParcelWhileSaving() throws Exception { - ParseFieldOperations.registerDefaultDecoders(); - ParseObject object, other; - List> tasks = new ArrayList<>(); - - // Mocked to let save work - ParseCurrentUserController userController = mock(ParseCurrentUserController.class); - when(userController.getAsync()).thenReturn(Task.forResult(null)); - ParseCorePlugins.getInstance().registerCurrentUserController(userController); - - // Mocked to simulate in-flight save - TaskCompletionSource tcs = new TaskCompletionSource<>(); - ParseObjectController objectController = mock(ParseObjectController.class); - when(objectController.saveAsync( - any(ParseObject.State.class), - any(ParseOperationSet.class), - anyString(), - any(ParseDecoder.class))) - .thenReturn(tcs.getTask()); - ParseCorePlugins.getInstance().registerObjectController(objectController); + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); // Create multiple ParseOperationSets - object = new ParseObject("TestObject"); + List> tasks = new ArrayList<>(); + ParseObject object = new ParseObject("TestObject"); object.setObjectId("id"); object.put("key", "value"); object.put("number", 5); @@ -624,12 +594,12 @@ public void testParcelWhileSaving() throws Exception { Parcel parcel = Parcel.obtain(); object.writeToParcel(parcel, 0); parcel.setDataPosition(0); - other = ParseObject.CREATOR.createFromParcel(parcel); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); assertTrue(other.isDirty("key")); assertTrue(other.isDirty("number")); assertEquals(other.getString("key"), "newValue"); assertEquals(other.getNumber("number"), 10); - // By design, we assume that old operations failed even if + // By design, when LDS is off, we assume that old operations failed even if // they are still running on the old instance. assertFalse(other.hasOutstandingOperations()); @@ -638,8 +608,117 @@ public void testParcelWhileSaving() throws Exception { ParseTaskUtils.wait(Task.whenAll(tasks)); } - // TODO test ParseGeoPoint and ParseFile after merge + @Test + public void testParcelWhileSavingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForSave(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + + object.put("key", "value"); + object.increment("number", 3); + Task saveTask = object.saveInBackground(); + assertTrue(object.hasOutstandingOperations()); // Saving + assertFalse(object.isDirty()); // Not dirty because it's saving + + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.hasOutstandingOperations()); // Still saving + assertFalse(other.isDirty()); // Still not dirty + assertEquals(other.getNumber("number"), 3); + + tcs.setResult(null); + saveTask.waitForCompletion(); + } + + @Test + public void testParcelWhileDeleting() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + Task deleteTask = object.deleteInBackground(); + + // ensure Log.w is called.. + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + // By design, when LDS is off, we assume that old operations failed even if + // they are still running on the old instance. + assertFalse(other.isDeleting); + assertTrue(object.isDeleting); + + tcs.setResult(null); + deleteTask.waitForCompletion(); + assertFalse(object.isDeleting); + assertTrue(object.isDeleted); + } + + @Test + public void testParcelWhileDeletingWithLDSEnabled() throws Exception { + mockCurrentUserController(); + TaskCompletionSource tcs = mockObjectControllerForDelete(); + + ParseObject object = new ParseObject("TestObject"); + object.setObjectId("id"); + OfflineStore lds = mock(OfflineStore.class); + when(lds.getObject("TestObject", "id")).thenReturn(object); + Parse.setLocalDatastore(lds); + Task deleteTask = object.deleteInBackground(); + + assertTrue(object.isDeleting); + Parcel parcel = Parcel.obtain(); + object.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ParseObject other = ParseObject.CREATOR.createFromParcel(parcel); + assertSame(object, other); + assertTrue(other.isDeleting); // Still deleting + + tcs.setResult(null); + deleteTask.waitForCompletion(); // complete deletion on original object. + assertFalse(other.isDeleting); + assertTrue(other.isDeleted); + } //endregion + private static void mockCurrentUserController() { + ParseCurrentUserController userController = mock(ParseCurrentUserController.class); + when(userController.getCurrentSessionTokenAsync()).thenReturn(Task.forResult("token")); + when(userController.getAsync()).thenReturn(Task.forResult(null)); + ParseCorePlugins.getInstance().registerCurrentUserController(userController); + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForSave() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.saveAsync( + any(ParseObject.State.class), any(ParseOperationSet.class), + anyString(), any(ParseDecoder.class)) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } + + // Returns a tcs to control the operation. + private static TaskCompletionSource mockObjectControllerForDelete() { + TaskCompletionSource tcs = new TaskCompletionSource<>(); + ParseObjectController objectController = mock(ParseObjectController.class); + when(objectController.deleteAsync( + any(ParseObject.State.class), anyString()) + ).thenReturn(tcs.getTask()); + ParseCorePlugins.getInstance().registerObjectController(objectController); + return tcs; + } } From e06280a6c6c0ff23f51986d36d09156c273c47f6 Mon Sep 17 00:00:00 2001 From: miav Date: Tue, 18 Apr 2017 11:58:35 +0200 Subject: [PATCH 10/10] Fix docs and tests --- Parse/src/main/java/com/parse/ParseParcelDecoder.java | 5 +++++ Parse/src/main/java/com/parse/ParseParcelEncoder.java | 8 ++++++-- Parse/src/test/java/com/parse/ParseACLTest.java | 2 +- Parse/src/test/java/com/parse/ParseObjectStateTest.java | 2 +- Parse/src/test/java/com/parse/ParseObjectTest.java | 4 +++- Parse/src/test/java/com/parse/ParseRelationTest.java | 2 +- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseParcelDecoder.java b/Parse/src/main/java/com/parse/ParseParcelDecoder.java index 09f65c1e7..cacc2344a 100644 --- a/Parse/src/main/java/com/parse/ParseParcelDecoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelDecoder.java @@ -21,7 +21,12 @@ * A {@code ParseParcelableDecoder} can be used to unparcel objects such as * {@link com.parse.ParseObject} from a {@link android.os.Parcel}. * + * This is capable of decoding objects and pointers to them. + * However, for improved behavior in the case of {@link ParseObject}s, use the stateful + * implementation {@link ParseObjectParcelDecoder}. + * * @see ParseParcelEncoder + * @see ParseObjectParcelDecoder */ /* package */ class ParseParcelDecoder { diff --git a/Parse/src/main/java/com/parse/ParseParcelEncoder.java b/Parse/src/main/java/com/parse/ParseParcelEncoder.java index 9eb0c04ed..f9124b91a 100644 --- a/Parse/src/main/java/com/parse/ParseParcelEncoder.java +++ b/Parse/src/main/java/com/parse/ParseParcelEncoder.java @@ -18,10 +18,14 @@ import java.util.Map; /** - * A {@code ParseParcelableEncoder} can be used to parcel objects such as - * {@link com.parse.ParseObject} into a {@link android.os.Parcel}. + * A {@code ParseParcelableEncoder} can be used to parcel objects into a {@link android.os.Parcel}. + * + * This is capable of parceling {@link ParseObject}s, but the result can likely be a + * {@link StackOverflowError} due to circular references in the objects tree. + * When needing to parcel {@link ParseObject}, use the stateful {@link ParseObjectParcelEncoder}. * * @see ParseParcelDecoder + * @see ParseObjectParcelEncoder */ /* package */ class ParseParcelEncoder { diff --git a/Parse/src/test/java/com/parse/ParseACLTest.java b/Parse/src/test/java/com/parse/ParseACLTest.java index c523b62e3..b34787a10 100644 --- a/Parse/src/test/java/com/parse/ParseACLTest.java +++ b/Parse/src/test/java/com/parse/ParseACLTest.java @@ -38,7 +38,7 @@ import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 23) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseACLTest { private final static String UNRESOLVED_KEY = "*unresolved"; diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index b4c9c2ccc..76b88b4c4 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -25,7 +25,7 @@ import static org.junit.Assert.assertTrue; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 23) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectStateTest { @Test diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index c56109c8d..1123adb26 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -49,7 +49,7 @@ import static org.mockito.Mockito.when; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 23) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseObjectTest { @Rule @@ -636,6 +636,7 @@ public void testParcelWhileSavingWithLDSEnabled() throws Exception { tcs.setResult(null); saveTask.waitForCompletion(); + Parse.setLocalDatastore(null); } @Test @@ -688,6 +689,7 @@ public void testParcelWhileDeletingWithLDSEnabled() throws Exception { deleteTask.waitForCompletion(); // complete deletion on original object. assertFalse(other.isDeleting); assertTrue(other.isDeleted); + Parse.setLocalDatastore(null); } //endregion diff --git a/Parse/src/test/java/com/parse/ParseRelationTest.java b/Parse/src/test/java/com/parse/ParseRelationTest.java index 3446a10c6..11fb7b05c 100644 --- a/Parse/src/test/java/com/parse/ParseRelationTest.java +++ b/Parse/src/test/java/com/parse/ParseRelationTest.java @@ -30,7 +30,7 @@ import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; @RunWith(RobolectricTestRunner.class) -@Config(constants = BuildConfig.class, sdk = 23) +@Config(constants = BuildConfig.class, sdk = TestHelper.ROBOLECTRIC_SDK_VERSION) public class ParseRelationTest { @Rule