From 301ec9378ac1b28955a2273cb9766b26630d1a35 Mon Sep 17 00:00:00 2001 From: miav Date: Sat, 11 Mar 2017 17:54:34 +0100 Subject: [PATCH 01/10] Add safeKeys that can be safely accessed. Exposing isDataAvailable(key) --- .../com/parse/NetworkQueryController.java | 2 + .../src/main/java/com/parse/ParseObject.java | 39 ++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Parse/src/main/java/com/parse/NetworkQueryController.java b/Parse/src/main/java/com/parse/NetworkQueryController.java index 2899b8b77..63b970c2c 100644 --- a/Parse/src/main/java/com/parse/NetworkQueryController.java +++ b/Parse/src/main/java/com/parse/NetworkQueryController.java @@ -134,8 +134,10 @@ public Integer then(Task task) throws Exception { if (resultClassName == null) { resultClassName = state.className(); } + JSONArray safeKeys = new JSONArray(state.selectedKeys()); for (int i = 0; i < results.length(); ++i) { JSONObject data = results.getJSONObject(i); + data.put("__safeKeys", safeKeys); T object = ParseObject.fromJSON(data, resultClassName, state.selectedKeys() == null); answer.add(object); diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index be28b75ed..37e78c974 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -98,6 +98,7 @@ public static Init newBuilder(String className) { private long createdAt = -1; private long updatedAt = -1; private boolean isComplete; + private Set safeKeys = new HashSet<>(); /* package */ Map serverData = new HashMap<>(); public Init(String className) { @@ -112,6 +113,7 @@ public Init(String className) { for (String key : state.keySet()) { serverData.put(key, state.get(key)); } + safeKeys = state.safeKeys(); isComplete = state.isComplete(); } @@ -159,6 +161,14 @@ public T remove(String key) { return self(); } + public T safeKeys(Collection keys) { + if (safeKeys == null) safeKeys = new HashSet<>(); + for (String key : keys) { + safeKeys.add(key); + } + return self(); + } + public T clear() { objectId = null; createdAt = -1; @@ -231,6 +241,7 @@ public State build() { private final long createdAt; private final long updatedAt; private final Map serverData; + private final Set safeKeys; private final boolean isComplete; /* package */ State(Init builder) { @@ -242,6 +253,7 @@ public State build() { : createdAt; serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData)); isComplete = builder.isComplete; + safeKeys = builder.safeKeys; } @SuppressWarnings("unchecked") @@ -277,6 +289,10 @@ public Set keySet() { return serverData.keySet(); } + public Set safeKeys() { + return safeKeys; + } + @Override public String toString() { return String.format(Locale.US, "%s@%s[" + @@ -921,6 +937,17 @@ protected boolean visit(Object object) { builder.put(KEY_ACL, acl); continue; } + if (key.equals("__safeKeys")) { + JSONArray safeKeys = json.getJSONArray(key); + if (safeKeys.length() > 0) { + Collection set = new HashSet<>(); + for (int i = 0; i < safeKeys.length(); i++) { + set.add(safeKeys.getString(i)); + } + builder.safeKeys(set); + } + continue; + } Object value = json.get(key); Object decodedObject = decoder.decode(value); @@ -3027,6 +3054,7 @@ public boolean containsKey(String key) { } } + /** * Access a {@link String} value. * @@ -3366,9 +3394,16 @@ public boolean isDataAvailable() { } } - /* package for tests */ boolean isDataAvailable(String key) { + /** + * Gets whether the {@code ParseObject} specified key has been fetched. + * This means the property can be accessed safely. + * + * @return {@code true} if the {@code ParseObject} key is new or has been fetched or refreshed. {@code false} + * otherwise. + */ + public boolean isDataAvailable(String key) { synchronized (mutex) { - return isDataAvailable() || estimatedData.containsKey(key); + return isDataAvailable() || estimatedData.containsKey(key) || state.safeKeys().contains(key); } } From 282752274765e6d06f5cb408970dc24637cb6567 Mon Sep 17 00:00:00 2001 From: miav Date: Sat, 11 Mar 2017 18:50:47 +0100 Subject: [PATCH 02/10] Add tests --- Parse/src/main/java/com/parse/ParseObject.java | 6 ++++-- .../src/test/java/com/parse/ParseObjectStateTest.java | 7 +++++++ Parse/src/test/java/com/parse/ParseObjectTest.java | 10 ++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 37e78c974..512587ab5 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -198,6 +198,7 @@ public T apply(State other) { for (String key : other.keySet()) { put(key, other.get(key)); } + safeKeys(other.safeKeys()); return self(); } @@ -297,7 +298,7 @@ public Set safeKeys() { public String toString() { return String.format(Locale.US, "%s@%s[" + "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + - "serverData=%s]", + "serverData=%s, safeKeys=%s]", getClass().getName(), Integer.toHexString(hashCode()), className, @@ -305,7 +306,8 @@ public String toString() { createdAt, updatedAt, isComplete, - serverData); + serverData, + safeKeys); } } diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index bd2d84ef2..e7cbee126 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -10,6 +10,7 @@ import org.junit.Test; +import java.util.Arrays; import java.util.Date; import static org.junit.Assert.assertEquals; @@ -30,6 +31,7 @@ public void testDefaults() { assertEquals(-1, state.updatedAt()); assertFalse(state.isComplete()); assertTrue(state.keySet().isEmpty()); + assertTrue(state.safeKeys().isEmpty()); } @Test @@ -62,6 +64,7 @@ public void testCopy() { .isComplete(true) .put("foo", "bar") .put("baz", "qux") + .safeKeys(Arrays.asList("safe", "keys")) .build(); ParseObject.State copy = new ParseObject.State.Builder(state).build(); assertEquals(state.className(), copy.className()); @@ -72,6 +75,9 @@ public void testCopy() { assertEquals(state.keySet().size(), copy.keySet().size()); assertEquals(state.get("foo"), copy.get("foo")); assertEquals(state.get("baz"), copy.get("baz")); + assertEquals(state.safeKeys().size(), copy.safeKeys().size()); + assertTrue(state.safeKeys().containsAll(copy.safeKeys())); + assertTrue(copy.safeKeys().containsAll(state.safeKeys())); } @Test @@ -121,5 +127,6 @@ public void testToString() { assertTrue(string.contains("updatedAt")); assertTrue(string.contains("isComplete")); assertTrue(string.contains("serverData")); + assertTrue(string.contains("safeKeys")); } } diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 863e00866..26a7a4ab3 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -237,7 +238,16 @@ public void testGetUnavailable() { ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.isComplete()).thenReturn(false); + ParseObject object = ParseObject.from(state); + object.get("foo"); + } + @Test + public void testGetAvailableIfKeySafe() { + ParseObject.State state = mock(ParseObject.State.class); + when(state.className()).thenReturn("TestObject"); + when(state.isComplete()).thenReturn(false); + when(state.safeKeys()).thenReturn(new HashSet<>(Arrays.asList("foo"))); ParseObject object = ParseObject.from(state); object.get("foo"); } From b71193ba204a6ffc1960dfa9651a8a346bad3078 Mon Sep 17 00:00:00 2001 From: miav Date: Sun, 12 Mar 2017 12:23:36 +0100 Subject: [PATCH 03/10] Support for LocalDataStore; fixed some bugs --- Parse/src/main/java/com/parse/NetworkQueryController.java | 2 +- Parse/src/main/java/com/parse/ParseObject.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Parse/src/main/java/com/parse/NetworkQueryController.java b/Parse/src/main/java/com/parse/NetworkQueryController.java index 63b970c2c..23985ba21 100644 --- a/Parse/src/main/java/com/parse/NetworkQueryController.java +++ b/Parse/src/main/java/com/parse/NetworkQueryController.java @@ -137,7 +137,7 @@ public Integer then(Task task) throws Exception { JSONArray safeKeys = new JSONArray(state.selectedKeys()); for (int i = 0; i < results.length(); ++i) { JSONObject data = results.getJSONObject(i); - data.put("__safeKeys", safeKeys); + data.put(ParseObject.KEY_SAFEKEYS, safeKeys); T object = ParseObject.fromJSON(data, resultClassName, state.selectedKeys() == null); answer.add(object); diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 512587ab5..8ca15ae72 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -64,6 +64,7 @@ public class ParseObject { */ private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; + /* package */ static final String KEY_SAFEKEYS = "__safeKeys"; /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; // Because Grantland messed up naming this... We'll only try to read from this for backward // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete @@ -175,6 +176,7 @@ public T clear() { updatedAt = -1; isComplete = false; serverData.clear(); + safeKeys.clear(); return self(); } @@ -254,7 +256,7 @@ public State build() { : createdAt; serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData)); isComplete = builder.isComplete; - safeKeys = builder.safeKeys; + safeKeys = new HashSet<>(builder.safeKeys); } @SuppressWarnings("unchecked") @@ -939,7 +941,7 @@ protected boolean visit(Object object) { builder.put(KEY_ACL, acl); continue; } - if (key.equals("__safeKeys")) { + if (key.equals(KEY_SAFEKEYS)) { JSONArray safeKeys = json.getJSONArray(key); if (safeKeys.length() > 0) { Collection set = new HashSet<>(); @@ -1018,6 +1020,8 @@ protected boolean visit(Object object) { // using the REST api and want to send data to Parse. json.put(KEY_COMPLETE, state.isComplete()); json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); + JSONArray safekeys = new JSONArray(state.safeKeys()); + json.put(KEY_SAFEKEYS, safekeys); // Operation Set Queue JSONArray operations = new JSONArray(); From 90877ae37ac75e1102392ad82c92bd6e7b8d093b Mon Sep 17 00:00:00 2001 From: miav Date: Sun, 12 Mar 2017 17:34:08 +0100 Subject: [PATCH 04/10] Support for dot notation in selected keys --- .../com/parse/NetworkQueryController.java | 7 ++-- .../src/main/java/com/parse/ParseDecoder.java | 2 +- .../src/main/java/com/parse/ParseObject.java | 38 ++++++++++++------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Parse/src/main/java/com/parse/NetworkQueryController.java b/Parse/src/main/java/com/parse/NetworkQueryController.java index 23985ba21..c61dc48cd 100644 --- a/Parse/src/main/java/com/parse/NetworkQueryController.java +++ b/Parse/src/main/java/com/parse/NetworkQueryController.java @@ -134,11 +134,12 @@ public Integer then(Task task) throws Exception { if (resultClassName == null) { resultClassName = state.className(); } - JSONArray safeKeys = new JSONArray(state.selectedKeys()); + boolean isSubset = state.selectedKeys() != null; + JSONArray selectedKeys = isSubset ? new JSONArray(state.selectedKeys()) : null; for (int i = 0; i < results.length(); ++i) { JSONObject data = results.getJSONObject(i); - data.put(ParseObject.KEY_SAFEKEYS, safeKeys); - T object = ParseObject.fromJSON(data, resultClassName, state.selectedKeys() == null); + if (isSubset) data.put(ParseObject.KEY_SELECTED_KEYS, selectedKeys); + T object = ParseObject.fromJSON(data, resultClassName, !isSubset); answer.add(object); /* diff --git a/Parse/src/main/java/com/parse/ParseDecoder.java b/Parse/src/main/java/com/parse/ParseDecoder.java index f522e6154..feebf667a 100644 --- a/Parse/src/main/java/com/parse/ParseDecoder.java +++ b/Parse/src/main/java/com/parse/ParseDecoder.java @@ -122,7 +122,7 @@ public Object decode(Object object) { } if (typeString.equals("Object")) { - return ParseObject.fromJSON(jsonObject, null, true, this); + return ParseObject.fromJSON(jsonObject, null, !jsonObject.has(ParseObject.KEY_SELECTED_KEYS), this); } if (typeString.equals("Relation")) { diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 8ca15ae72..3b1d0c7c6 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -64,7 +64,7 @@ public class ParseObject { */ private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; - /* package */ static final String KEY_SAFEKEYS = "__safeKeys"; + /* package */ static final String KEY_SELECTED_KEYS = "__selectedKeys"; /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; // Because Grantland messed up naming this... We'll only try to read from this for backward // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete @@ -154,6 +154,7 @@ public T isComplete(boolean complete) { public T put(String key, Object value) { serverData.put(key, value); + safeKeys.remove(key); return self(); } @@ -165,7 +166,7 @@ public T remove(String key) { public T safeKeys(Collection keys) { if (safeKeys == null) safeKeys = new HashSet<>(); for (String key : keys) { - safeKeys.add(key); + if (!serverData.containsKey(key)) safeKeys.add(key); } return self(); } @@ -598,22 +599,20 @@ public Void then(Task task) throws Exception { /** * Creates a new {@code ParseObject} based on data from the Parse server. - * * @param json * The object's data. * @param defaultClassName * The className of the object, if none is in the JSON. * @param isComplete - * {@code true} if this is all of the data on the server for the object. + * {@code true} if this is all of the data on the server for the object. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, - boolean isComplete) { + boolean isComplete) { return fromJSON(json, defaultClassName, isComplete, ParseDecoder.get()); } /** * Creates a new {@code ParseObject} based on data from the Parse server. - * * @param json * The object's data. * @param defaultClassName @@ -621,10 +620,9 @@ public Void then(Task task) throws Exception { * @param isComplete * {@code true} if this is all of the data on the server for the object. * @param decoder - * Delegate for knowing how to decode the values in the JSON. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, - boolean isComplete, ParseDecoder decoder) { + boolean isComplete, ParseDecoder decoder) { String className = json.optString(KEY_CLASS_NAME, defaultClassName); if (className == null) { return null; @@ -896,9 +894,9 @@ protected boolean visit(Object object) { } } + /** * Merges from JSON in REST format. - * * Updates this object with data from the server. * * @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder) @@ -941,12 +939,14 @@ protected boolean visit(Object object) { builder.put(KEY_ACL, acl); continue; } - if (key.equals(KEY_SAFEKEYS)) { + if (key.equals(KEY_SELECTED_KEYS)) { JSONArray safeKeys = json.getJSONArray(key); if (safeKeys.length() > 0) { Collection set = new HashSet<>(); for (int i = 0; i < safeKeys.length(); i++) { - set.add(safeKeys.getString(i)); + // Don't add nested keys. + String safeKey = safeKeys.getString(i); + if (!safeKey.contains(".")) set.add(safeKey); } builder.safeKeys(set); } @@ -954,6 +954,18 @@ protected boolean visit(Object object) { } Object value = json.get(key); + if (value instanceof JSONObject && json.has(KEY_SELECTED_KEYS)) { + // This might be a ParseObject. Pass selected keys to understand if it is complete. + JSONArray selectedKeys = json.getJSONArray(KEY_SELECTED_KEYS); + JSONArray nestedKeys = new JSONArray(); + for (int i = 0; i < selectedKeys.length(); i++) { + String nestedKey = selectedKeys.getString(i); + if (nestedKey.startsWith(key+".")) nestedKeys.put(nestedKey.substring(key.length()+1)); + } + if (nestedKeys.length() > 0) { + ((JSONObject) value).put(KEY_SELECTED_KEYS, nestedKeys); + } + } Object decodedObject = decoder.decode(value); builder.put(key, decodedObject); } @@ -1021,7 +1033,7 @@ protected boolean visit(Object object) { json.put(KEY_COMPLETE, state.isComplete()); json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); JSONArray safekeys = new JSONArray(state.safeKeys()); - json.put(KEY_SAFEKEYS, safekeys); + json.put(KEY_SELECTED_KEYS, safekeys); // Operation Set Queue JSONArray operations = new JSONArray(); @@ -2896,7 +2908,7 @@ public void put(String key, Object value) { if (value instanceof JSONObject) { ParseDecoder decoder = ParseDecoder.get(); value = decoder.convertJSONObjectToMap((JSONObject) value); - } else if (value instanceof JSONArray){ + } else if (value instanceof JSONArray) { ParseDecoder decoder = ParseDecoder.get(); value = decoder.convertJSONArrayToList((JSONArray) value); } From f009d3440b271492603a12a4b9ef26eb2000d9b0 Mon Sep 17 00:00:00 2001 From: miav Date: Sun, 12 Mar 2017 19:14:34 +0100 Subject: [PATCH 05/10] Addressing nested keys without parent key --- Parse/src/main/java/com/parse/ParseObject.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 3b1d0c7c6..cf3d13f4d 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -946,7 +946,8 @@ protected boolean visit(Object object) { for (int i = 0; i < safeKeys.length(); i++) { // Don't add nested keys. String safeKey = safeKeys.getString(i); - if (!safeKey.contains(".")) set.add(safeKey); + if (safeKey.contains(".")) safeKey = safeKey.split(".")[0]; + set.add(safeKey); } builder.safeKeys(set); } From bc1c1faf06fa21867a2fb55ae08db04568838db5 Mon Sep 17 00:00:00 2001 From: miav Date: Sun, 12 Mar 2017 19:57:32 +0100 Subject: [PATCH 06/10] Adding extra tests --- .../test/java/com/parse/ParseDecoderTest.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Parse/src/test/java/com/parse/ParseDecoderTest.java b/Parse/src/test/java/com/parse/ParseDecoderTest.java index 8a8cc3920..0526908a6 100644 --- a/Parse/src/test/java/com/parse/ParseDecoderTest.java +++ b/Parse/src/test/java/com/parse/ParseDecoderTest.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; // For android.util.Base64 @RunWith(RobolectricTestRunner.class) @@ -198,6 +199,46 @@ public void testParseObject() throws JSONException { assertNotNull(parseObject); } + @Test + public void testIncludedParseObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + JSONObject child = new JSONObject(); + child.put("__type", "Object"); + child.put("className", "GameScore"); + child.put("createdAt", "2015-06-22T21:23:41.733Z"); + child.put("objectId", "TT1ZskATqR"); + child.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + json.put("child", child); + ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertNotNull(parseObject.getParseObject("child")); + } + + @Test + public void testCompleteness() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + json.put("foo", "foo"); + json.put("bar", "bar"); + ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertTrue(parseObject.isDataAvailable()); + + JSONArray arr = new JSONArray("[\"foo\"]"); + json.put("__selectedKeys", arr); + parseObject = (ParseObject) ParseDecoder.get().decode(json); + assertFalse(parseObject.isDataAvailable()); + } + @Test public void testRelation() throws JSONException { JSONObject json = new JSONObject(); From 7d7cc45a6c98599c50400e68db28eb32924f17b5 Mon Sep 17 00:00:00 2001 From: miav Date: Sun, 12 Mar 2017 22:55:39 +0100 Subject: [PATCH 07/10] New test, fixed a bug --- .../src/main/java/com/parse/ParseObject.java | 5 ++-- .../test/java/com/parse/ParseDecoderTest.java | 28 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index cf3d13f4d..683dcbfb1 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -620,6 +620,7 @@ public Void then(Task task) throws Exception { * @param isComplete * {@code true} if this is all of the data on the server for the object. * @param decoder + * Delegate for knowing how to decode the values in the JSON. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, boolean isComplete, ParseDecoder decoder) { @@ -946,7 +947,7 @@ protected boolean visit(Object object) { for (int i = 0; i < safeKeys.length(); i++) { // Don't add nested keys. String safeKey = safeKeys.getString(i); - if (safeKey.contains(".")) safeKey = safeKey.split(".")[0]; + if (safeKey.contains(".")) safeKey = safeKey.split("\\.")[0]; set.add(safeKey); } builder.safeKeys(set); @@ -961,7 +962,7 @@ protected boolean visit(Object object) { JSONArray nestedKeys = new JSONArray(); for (int i = 0; i < selectedKeys.length(); i++) { String nestedKey = selectedKeys.getString(i); - if (nestedKey.startsWith(key+".")) nestedKeys.put(nestedKey.substring(key.length()+1)); + if (nestedKey.startsWith(key + ".")) nestedKeys.put(nestedKey.substring(key.length() + 1)); } if (nestedKeys.length() > 0) { ((JSONObject) value).put(KEY_SELECTED_KEYS, nestedKeys); diff --git a/Parse/src/test/java/com/parse/ParseDecoderTest.java b/Parse/src/test/java/com/parse/ParseDecoderTest.java index 0526908a6..0c8bf4013 100644 --- a/Parse/src/test/java/com/parse/ParseDecoderTest.java +++ b/Parse/src/test/java/com/parse/ParseDecoderTest.java @@ -239,6 +239,34 @@ public void testCompleteness() throws JSONException { assertFalse(parseObject.isDataAvailable()); } + @Test + public void testCompletenessOfIncludedParseObject() throws JSONException { + JSONObject json = new JSONObject(); + json.put("__type", "Object"); + json.put("className", "GameScore"); + json.put("createdAt", "2015-06-22T21:23:41.733Z"); + json.put("objectId", "TT1ZskATqS"); + json.put("updatedAt", "2015-06-22T22:06:18.104Z"); + + JSONObject child = new JSONObject(); + child.put("__type", "Object"); + child.put("className", "GameScore"); + child.put("createdAt", "2015-06-22T21:23:41.733Z"); + child.put("objectId", "TT1ZskATqR"); + child.put("updatedAt", "2015-06-22T22:06:18.104Z"); + child.put("bar", "child bar"); + + JSONArray arr = new JSONArray("[\"foo.bar\"]"); + json.put("foo", child); + json.put("__selectedKeys", arr); + ParseObject parentObject = (ParseObject) ParseDecoder.get().decode(json); + assertFalse(parentObject.isDataAvailable()); + assertTrue(parentObject.isDataAvailable("foo")); + ParseObject childObject = parentObject.getParseObject("foo"); + assertFalse(childObject.isDataAvailable()); + assertTrue(childObject.isDataAvailable("bar")); + } + @Test public void testRelation() throws JSONException { JSONObject json = new JSONObject(); From 6d2ccdee2d4e21cebe8af8ae4186abd813681af2 Mon Sep 17 00:00:00 2001 From: miav Date: Mon, 13 Mar 2017 01:01:41 +0100 Subject: [PATCH 08/10] New signature for fromJSON --- .../com/parse/NetworkQueryController.java | 5 +-- .../src/main/java/com/parse/ParseDecoder.java | 2 +- .../src/main/java/com/parse/ParseObject.java | 33 +++++++++++++------ 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Parse/src/main/java/com/parse/NetworkQueryController.java b/Parse/src/main/java/com/parse/NetworkQueryController.java index c61dc48cd..5e166d9ab 100644 --- a/Parse/src/main/java/com/parse/NetworkQueryController.java +++ b/Parse/src/main/java/com/parse/NetworkQueryController.java @@ -134,12 +134,9 @@ public Integer then(Task task) throws Exception { if (resultClassName == null) { resultClassName = state.className(); } - boolean isSubset = state.selectedKeys() != null; - JSONArray selectedKeys = isSubset ? new JSONArray(state.selectedKeys()) : null; for (int i = 0; i < results.length(); ++i) { JSONObject data = results.getJSONObject(i); - if (isSubset) data.put(ParseObject.KEY_SELECTED_KEYS, selectedKeys); - T object = ParseObject.fromJSON(data, resultClassName, !isSubset); + T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys()); answer.add(object); /* diff --git a/Parse/src/main/java/com/parse/ParseDecoder.java b/Parse/src/main/java/com/parse/ParseDecoder.java index feebf667a..bf7b0c6b0 100644 --- a/Parse/src/main/java/com/parse/ParseDecoder.java +++ b/Parse/src/main/java/com/parse/ParseDecoder.java @@ -122,7 +122,7 @@ public Object decode(Object object) { } if (typeString.equals("Object")) { - return ParseObject.fromJSON(jsonObject, null, !jsonObject.has(ParseObject.KEY_SELECTED_KEYS), this); + return ParseObject.fromJSON(jsonObject, null, this); } if (typeString.equals("Relation")) { diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 683dcbfb1..cc73ba6dc 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -64,7 +64,7 @@ public class ParseObject { */ private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; - /* package */ static final String KEY_SELECTED_KEYS = "__selectedKeys"; + private static final String KEY_SELECTED_KEYS = "__selectedKeys"; /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; // Because Grantland messed up naming this... We'll only try to read from this for backward // compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete @@ -603,32 +603,45 @@ public Void then(Task task) throws Exception { * The object's data. * @param defaultClassName * The className of the object, if none is in the JSON. - * @param isComplete - * {@code true} if this is all of the data on the server for the object. + * @param decoder + * Delegate for knowing how to decode the values in the JSON. + * @param selectedKeys + * Set of keys selected when quering for this object. If none, the object is assumed to + * be complete, i.e. this is all the data for the object on the server. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, - boolean isComplete) { - return fromJSON(json, defaultClassName, isComplete, ParseDecoder.get()); + ParseDecoder decoder, + Set selectedKeys) { + boolean complete = selectedKeys == null || selectedKeys.isEmpty(); + if (!complete) { + JSONArray keys = new JSONArray(selectedKeys); + try { + json.put(KEY_SELECTED_KEYS, keys); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + return fromJSON(json, defaultClassName, decoder); } /** * Creates a new {@code ParseObject} based on data from the Parse server. * @param json - * The object's data. + * The object's data. It is assumed to be complete, unless the JSON has the + * {@link #KEY_SELECTED_KEYS} key. * @param defaultClassName * The className of the object, if none is in the JSON. - * @param isComplete - * {@code true} if this is all of the data on the server for the object. * @param decoder * Delegate for knowing how to decode the values in the JSON. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, - boolean isComplete, ParseDecoder decoder) { + ParseDecoder decoder) { String className = json.optString(KEY_CLASS_NAME, defaultClassName); if (className == null) { return null; } String objectId = json.optString(KEY_OBJECT_ID, null); + boolean isComplete = !json.has(KEY_SELECTED_KEYS); @SuppressWarnings("unchecked") T object = (T) ParseObject.createWithoutData(className, objectId); State newState = object.mergeFromServer(object.getState(), json, decoder, isComplete); @@ -641,7 +654,7 @@ public Void then(Task task) throws Exception { * * Method is used by parse server webhooks implementation to create a * new {@code ParseObject} from the incoming json payload. The method is different from - * {@link #fromJSON(JSONObject, String, boolean)} ()} in that it calls + * {@link #fromJSON(JSONObject, String, ParseDecoder, Set)} ()} in that it calls * {@link #build(JSONObject, ParseDecoder)} which populates operation queue * rather then the server data from the incoming JSON, as at external server the incoming * JSON may not represent the actual server data. Also it handles From fcf24e3af94f0321d80d399c418a325e7e3da90f Mon Sep 17 00:00:00 2001 From: miav Date: Mon, 13 Mar 2017 02:50:40 +0100 Subject: [PATCH 09/10] Added some comments --- Parse/src/main/java/com/parse/ParseObject.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index cc73ba6dc..373403e70 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -64,6 +64,8 @@ public class ParseObject { */ private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; + // Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s + // correctly, and helps constructing the {@code State.safeKeys()} set. private static final String KEY_SELECTED_KEYS = "__selectedKeys"; /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; // Because Grantland messed up naming this... We'll only try to read from this for backward @@ -293,6 +295,10 @@ public Set keySet() { return serverData.keySet(); } + // Extra keys that are undefined for this object, but that can be accessed without throwing. + // These come e.g. from ParseQuery.selectKeys(). Selected keys must be available to get() + // methods even if undefined, for consistency with complete objects. + // For a complete object, this set is empty. public Set safeKeys() { return safeKeys; } @@ -612,8 +618,7 @@ public Void then(Task task) throws Exception { /* package */ static T fromJSON(JSONObject json, String defaultClassName, ParseDecoder decoder, Set selectedKeys) { - boolean complete = selectedKeys == null || selectedKeys.isEmpty(); - if (!complete) { + if (selectedKeys != null && !selectedKeys.isEmpty()) { JSONArray keys = new JSONArray(selectedKeys); try { json.put(KEY_SELECTED_KEYS, keys); From b144ca0437518ad35906f44efba50a6b9f67eb90 Mon Sep 17 00:00:00 2001 From: miav Date: Mon, 13 Mar 2017 11:30:12 +0100 Subject: [PATCH 10/10] Refactored safeKeys() into availableKeys() --- .../src/main/java/com/parse/ParseObject.java | 48 ++++++++++--------- .../java/com/parse/ParseObjectStateTest.java | 13 +++-- .../test/java/com/parse/ParseObjectTest.java | 4 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/Parse/src/main/java/com/parse/ParseObject.java b/Parse/src/main/java/com/parse/ParseObject.java index 373403e70..a6b0f673f 100644 --- a/Parse/src/main/java/com/parse/ParseObject.java +++ b/Parse/src/main/java/com/parse/ParseObject.java @@ -65,7 +65,7 @@ public class ParseObject { private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; // Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s - // correctly, and helps constructing the {@code State.safeKeys()} set. + // correctly, and helps constructing the {@code State.availableKeys()} set. private static final String KEY_SELECTED_KEYS = "__selectedKeys"; /* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually"; // Because Grantland messed up naming this... We'll only try to read from this for backward @@ -101,7 +101,7 @@ public static Init newBuilder(String className) { private long createdAt = -1; private long updatedAt = -1; private boolean isComplete; - private Set safeKeys = new HashSet<>(); + private Set availableKeys = new HashSet<>(); /* package */ Map serverData = new HashMap<>(); public Init(String className) { @@ -113,10 +113,11 @@ public Init(String className) { objectId = state.objectId(); createdAt = state.createdAt(); updatedAt = state.updatedAt(); + availableKeys = state.availableKeys(); for (String key : state.keySet()) { serverData.put(key, state.get(key)); + availableKeys.add(key); } - safeKeys = state.safeKeys(); isComplete = state.isComplete(); } @@ -156,7 +157,7 @@ public T isComplete(boolean complete) { public T put(String key, Object value) { serverData.put(key, value); - safeKeys.remove(key); + availableKeys.add(key); return self(); } @@ -165,10 +166,9 @@ public T remove(String key) { return self(); } - public T safeKeys(Collection keys) { - if (safeKeys == null) safeKeys = new HashSet<>(); + public T availableKeys(Collection keys) { for (String key : keys) { - if (!serverData.containsKey(key)) safeKeys.add(key); + availableKeys.add(key); } return self(); } @@ -179,7 +179,7 @@ public T clear() { updatedAt = -1; isComplete = false; serverData.clear(); - safeKeys.clear(); + availableKeys.clear(); return self(); } @@ -203,7 +203,7 @@ public T apply(State other) { for (String key : other.keySet()) { put(key, other.get(key)); } - safeKeys(other.safeKeys()); + availableKeys(other.availableKeys()); return self(); } @@ -247,7 +247,7 @@ public State build() { private final long createdAt; private final long updatedAt; private final Map serverData; - private final Set safeKeys; + private final Set availableKeys; private final boolean isComplete; /* package */ State(Init builder) { @@ -259,7 +259,7 @@ public State build() { : createdAt; serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData)); isComplete = builder.isComplete; - safeKeys = new HashSet<>(builder.safeKeys); + availableKeys = new HashSet<>(builder.availableKeys); } @SuppressWarnings("unchecked") @@ -295,19 +295,20 @@ public Set keySet() { return serverData.keySet(); } - // Extra keys that are undefined for this object, but that can be accessed without throwing. - // These come e.g. from ParseQuery.selectKeys(). Selected keys must be available to get() - // methods even if undefined, for consistency with complete objects. - // For a complete object, this set is empty. - public Set safeKeys() { - return safeKeys; + // Available keys for this object. With respect to keySet(), this includes also keys that are + // undefined in the server, but that should be accessed without throwing. + // These extra keys come e.g. from ParseQuery.selectKeys(). Selected keys must be available to + // get() methods even if undefined, for consistency with complete objects. + // For a complete object, this set is equal to keySet(). + public Set availableKeys() { + return availableKeys; } @Override public String toString() { return String.format(Locale.US, "%s@%s[" + "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + - "serverData=%s, safeKeys=%s]", + "serverData=%s, availableKeys=%s]", getClass().getName(), Integer.toHexString(hashCode()), className, @@ -316,7 +317,7 @@ public String toString() { updatedAt, isComplete, serverData, - safeKeys); + availableKeys); } } @@ -968,7 +969,7 @@ protected boolean visit(Object object) { if (safeKey.contains(".")) safeKey = safeKey.split("\\.")[0]; set.add(safeKey); } - builder.safeKeys(set); + builder.availableKeys(set); } continue; } @@ -1052,8 +1053,8 @@ protected boolean visit(Object object) { // using the REST api and want to send data to Parse. json.put(KEY_COMPLETE, state.isComplete()); json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); - JSONArray safekeys = new JSONArray(state.safeKeys()); - json.put(KEY_SELECTED_KEYS, safekeys); + JSONArray availableKeys = new JSONArray(state.availableKeys()); + json.put(KEY_SELECTED_KEYS, availableKeys); // Operation Set Queue JSONArray operations = new JSONArray(); @@ -3441,7 +3442,8 @@ public boolean isDataAvailable() { */ public boolean isDataAvailable(String key) { synchronized (mutex) { - return isDataAvailable() || estimatedData.containsKey(key) || state.safeKeys().contains(key); + // Fallback to estimatedData to include dirty changes. + return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key); } } diff --git a/Parse/src/test/java/com/parse/ParseObjectStateTest.java b/Parse/src/test/java/com/parse/ParseObjectStateTest.java index e7cbee126..e4c906301 100644 --- a/Parse/src/test/java/com/parse/ParseObjectStateTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectStateTest.java @@ -18,7 +18,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.contains; public class ParseObjectStateTest { @@ -31,7 +30,7 @@ public void testDefaults() { assertEquals(-1, state.updatedAt()); assertFalse(state.isComplete()); assertTrue(state.keySet().isEmpty()); - assertTrue(state.safeKeys().isEmpty()); + assertTrue(state.availableKeys().isEmpty()); } @Test @@ -64,7 +63,7 @@ public void testCopy() { .isComplete(true) .put("foo", "bar") .put("baz", "qux") - .safeKeys(Arrays.asList("safe", "keys")) + .availableKeys(Arrays.asList("safe", "keys")) .build(); ParseObject.State copy = new ParseObject.State.Builder(state).build(); assertEquals(state.className(), copy.className()); @@ -75,9 +74,9 @@ public void testCopy() { assertEquals(state.keySet().size(), copy.keySet().size()); assertEquals(state.get("foo"), copy.get("foo")); assertEquals(state.get("baz"), copy.get("baz")); - assertEquals(state.safeKeys().size(), copy.safeKeys().size()); - assertTrue(state.safeKeys().containsAll(copy.safeKeys())); - assertTrue(copy.safeKeys().containsAll(state.safeKeys())); + assertEquals(state.availableKeys().size(), copy.availableKeys().size()); + assertTrue(state.availableKeys().containsAll(copy.availableKeys())); + assertTrue(copy.availableKeys().containsAll(state.availableKeys())); } @Test @@ -127,6 +126,6 @@ public void testToString() { assertTrue(string.contains("updatedAt")); assertTrue(string.contains("isComplete")); assertTrue(string.contains("serverData")); - assertTrue(string.contains("safeKeys")); + assertTrue(string.contains("availableKeys")); } } diff --git a/Parse/src/test/java/com/parse/ParseObjectTest.java b/Parse/src/test/java/com/parse/ParseObjectTest.java index 26a7a4ab3..24aded591 100644 --- a/Parse/src/test/java/com/parse/ParseObjectTest.java +++ b/Parse/src/test/java/com/parse/ParseObjectTest.java @@ -243,11 +243,11 @@ public void testGetUnavailable() { } @Test - public void testGetAvailableIfKeySafe() { + public void testGetAvailableIfKeyAvailable() { ParseObject.State state = mock(ParseObject.State.class); when(state.className()).thenReturn("TestObject"); when(state.isComplete()).thenReturn(false); - when(state.safeKeys()).thenReturn(new HashSet<>(Arrays.asList("foo"))); + when(state.availableKeys()).thenReturn(new HashSet<>(Arrays.asList("foo"))); ParseObject object = ParseObject.from(state); object.get("foo"); }