Skip to content

Commit ddf8de4

Browse files
natario1rogerhu
authored andcommitted
Adding availableKeys to ParseObject.State (#596)
* Add safeKeys that can be safely accessed. Exposing isDataAvailable(key) * Add tests * Support for LocalDataStore; fixed some bugs * Support for dot notation in selected keys * Addressing nested keys without parent key * Adding extra tests * New test, fixed a bug * New signature for fromJSON * Added some comments * Refactored safeKeys() into availableKeys()
1 parent 809b829 commit ddf8de4

File tree

6 files changed

+180
-20
lines changed

6 files changed

+180
-20
lines changed

Parse/src/main/java/com/parse/NetworkQueryController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public Integer then(Task<JSONObject> task) throws Exception {
136136
}
137137
for (int i = 0; i < results.length(); ++i) {
138138
JSONObject data = results.getJSONObject(i);
139-
T object = ParseObject.fromJSON(data, resultClassName, state.selectedKeys() == null);
139+
T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys());
140140
answer.add(object);
141141

142142
/*

Parse/src/main/java/com/parse/ParseDecoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public Object decode(Object object) {
122122
}
123123

124124
if (typeString.equals("Object")) {
125-
return ParseObject.fromJSON(jsonObject, null, true, this);
125+
return ParseObject.fromJSON(jsonObject, null, this);
126126
}
127127

128128
if (typeString.equals("Relation")) {

Parse/src/main/java/com/parse/ParseObject.java

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public class ParseObject {
6464
*/
6565
private static final String KEY_COMPLETE = "__complete";
6666
private static final String KEY_OPERATIONS = "__operations";
67+
// Array of keys selected when querying for the object. Helps decoding nested {@code ParseObject}s
68+
// correctly, and helps constructing the {@code State.availableKeys()} set.
69+
private static final String KEY_SELECTED_KEYS = "__selectedKeys";
6770
/* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually";
6871
// Because Grantland messed up naming this... We'll only try to read from this for backward
6972
// compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete
@@ -98,6 +101,7 @@ public static Init<?> newBuilder(String className) {
98101
private long createdAt = -1;
99102
private long updatedAt = -1;
100103
private boolean isComplete;
104+
private Set<String> availableKeys = new HashSet<>();
101105
/* package */ Map<String, Object> serverData = new HashMap<>();
102106

103107
public Init(String className) {
@@ -109,8 +113,10 @@ public Init(String className) {
109113
objectId = state.objectId();
110114
createdAt = state.createdAt();
111115
updatedAt = state.updatedAt();
116+
availableKeys = state.availableKeys();
112117
for (String key : state.keySet()) {
113118
serverData.put(key, state.get(key));
119+
availableKeys.add(key);
114120
}
115121
isComplete = state.isComplete();
116122
}
@@ -151,6 +157,7 @@ public T isComplete(boolean complete) {
151157

152158
public T put(String key, Object value) {
153159
serverData.put(key, value);
160+
availableKeys.add(key);
154161
return self();
155162
}
156163

@@ -159,12 +166,20 @@ public T remove(String key) {
159166
return self();
160167
}
161168

169+
public T availableKeys(Collection<String> keys) {
170+
for (String key : keys) {
171+
availableKeys.add(key);
172+
}
173+
return self();
174+
}
175+
162176
public T clear() {
163177
objectId = null;
164178
createdAt = -1;
165179
updatedAt = -1;
166180
isComplete = false;
167181
serverData.clear();
182+
availableKeys.clear();
168183
return self();
169184
}
170185

@@ -188,6 +203,7 @@ public T apply(State other) {
188203
for (String key : other.keySet()) {
189204
put(key, other.get(key));
190205
}
206+
availableKeys(other.availableKeys());
191207
return self();
192208
}
193209

@@ -231,6 +247,7 @@ public State build() {
231247
private final long createdAt;
232248
private final long updatedAt;
233249
private final Map<String, Object> serverData;
250+
private final Set<String> availableKeys;
234251
private final boolean isComplete;
235252

236253
/* package */ State(Init<?> builder) {
@@ -242,6 +259,7 @@ public State build() {
242259
: createdAt;
243260
serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData));
244261
isComplete = builder.isComplete;
262+
availableKeys = new HashSet<>(builder.availableKeys);
245263
}
246264

247265
@SuppressWarnings("unchecked")
@@ -277,19 +295,29 @@ public Set<String> keySet() {
277295
return serverData.keySet();
278296
}
279297

298+
// Available keys for this object. With respect to keySet(), this includes also keys that are
299+
// undefined in the server, but that should be accessed without throwing.
300+
// These extra keys come e.g. from ParseQuery.selectKeys(). Selected keys must be available to
301+
// get() methods even if undefined, for consistency with complete objects.
302+
// For a complete object, this set is equal to keySet().
303+
public Set<String> availableKeys() {
304+
return availableKeys;
305+
}
306+
280307
@Override
281308
public String toString() {
282309
return String.format(Locale.US, "%s@%s[" +
283310
"className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " +
284-
"serverData=%s]",
311+
"serverData=%s, availableKeys=%s]",
285312
getClass().getName(),
286313
Integer.toHexString(hashCode()),
287314
className,
288315
objectId,
289316
createdAt,
290317
updatedAt,
291318
isComplete,
292-
serverData);
319+
serverData,
320+
availableKeys);
293321
}
294322
}
295323

@@ -578,38 +606,48 @@ public Void then(Task<Void> task) throws Exception {
578606

579607
/**
580608
* Creates a new {@code ParseObject} based on data from the Parse server.
581-
*
582609
* @param json
583610
* The object's data.
584611
* @param defaultClassName
585612
* The className of the object, if none is in the JSON.
586-
* @param isComplete
587-
* {@code true} if this is all of the data on the server for the object.
613+
* @param decoder
614+
* Delegate for knowing how to decode the values in the JSON.
615+
* @param selectedKeys
616+
* Set of keys selected when quering for this object. If none, the object is assumed to
617+
* be complete, i.e. this is all the data for the object on the server.
588618
*/
589619
/* package */ static <T extends ParseObject> T fromJSON(JSONObject json, String defaultClassName,
590-
boolean isComplete) {
591-
return fromJSON(json, defaultClassName, isComplete, ParseDecoder.get());
620+
ParseDecoder decoder,
621+
Set<String> selectedKeys) {
622+
if (selectedKeys != null && !selectedKeys.isEmpty()) {
623+
JSONArray keys = new JSONArray(selectedKeys);
624+
try {
625+
json.put(KEY_SELECTED_KEYS, keys);
626+
} catch (JSONException e) {
627+
throw new RuntimeException(e);
628+
}
629+
}
630+
return fromJSON(json, defaultClassName, decoder);
592631
}
593632

594633
/**
595634
* Creates a new {@code ParseObject} based on data from the Parse server.
596-
*
597635
* @param json
598-
* The object's data.
636+
* The object's data. It is assumed to be complete, unless the JSON has the
637+
* {@link #KEY_SELECTED_KEYS} key.
599638
* @param defaultClassName
600639
* The className of the object, if none is in the JSON.
601-
* @param isComplete
602-
* {@code true} if this is all of the data on the server for the object.
603640
* @param decoder
604641
* Delegate for knowing how to decode the values in the JSON.
605642
*/
606643
/* package */ static <T extends ParseObject> T fromJSON(JSONObject json, String defaultClassName,
607-
boolean isComplete, ParseDecoder decoder) {
644+
ParseDecoder decoder) {
608645
String className = json.optString(KEY_CLASS_NAME, defaultClassName);
609646
if (className == null) {
610647
return null;
611648
}
612649
String objectId = json.optString(KEY_OBJECT_ID, null);
650+
boolean isComplete = !json.has(KEY_SELECTED_KEYS);
613651
@SuppressWarnings("unchecked")
614652
T object = (T) ParseObject.createWithoutData(className, objectId);
615653
State newState = object.mergeFromServer(object.getState(), json, decoder, isComplete);
@@ -622,7 +660,7 @@ public Void then(Task<Void> task) throws Exception {
622660
*
623661
* Method is used by parse server webhooks implementation to create a
624662
* new {@code ParseObject} from the incoming json payload. The method is different from
625-
* {@link #fromJSON(JSONObject, String, boolean)} ()} in that it calls
663+
* {@link #fromJSON(JSONObject, String, ParseDecoder, Set)} ()} in that it calls
626664
* {@link #build(JSONObject, ParseDecoder)} which populates operation queue
627665
* rather then the server data from the incoming JSON, as at external server the incoming
628666
* JSON may not represent the actual server data. Also it handles
@@ -876,9 +914,9 @@ protected boolean visit(Object object) {
876914
}
877915
}
878916

917+
879918
/**
880919
* Merges from JSON in REST format.
881-
*
882920
* Updates this object with data from the server.
883921
*
884922
* @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder)
@@ -921,8 +959,34 @@ protected boolean visit(Object object) {
921959
builder.put(KEY_ACL, acl);
922960
continue;
923961
}
962+
if (key.equals(KEY_SELECTED_KEYS)) {
963+
JSONArray safeKeys = json.getJSONArray(key);
964+
if (safeKeys.length() > 0) {
965+
Collection<String> set = new HashSet<>();
966+
for (int i = 0; i < safeKeys.length(); i++) {
967+
// Don't add nested keys.
968+
String safeKey = safeKeys.getString(i);
969+
if (safeKey.contains(".")) safeKey = safeKey.split("\\.")[0];
970+
set.add(safeKey);
971+
}
972+
builder.availableKeys(set);
973+
}
974+
continue;
975+
}
924976

925977
Object value = json.get(key);
978+
if (value instanceof JSONObject && json.has(KEY_SELECTED_KEYS)) {
979+
// This might be a ParseObject. Pass selected keys to understand if it is complete.
980+
JSONArray selectedKeys = json.getJSONArray(KEY_SELECTED_KEYS);
981+
JSONArray nestedKeys = new JSONArray();
982+
for (int i = 0; i < selectedKeys.length(); i++) {
983+
String nestedKey = selectedKeys.getString(i);
984+
if (nestedKey.startsWith(key + ".")) nestedKeys.put(nestedKey.substring(key.length() + 1));
985+
}
986+
if (nestedKeys.length() > 0) {
987+
((JSONObject) value).put(KEY_SELECTED_KEYS, nestedKeys);
988+
}
989+
}
926990
Object decodedObject = decoder.decode(value);
927991
builder.put(key, decodedObject);
928992
}
@@ -989,6 +1053,8 @@ protected boolean visit(Object object) {
9891053
// using the REST api and want to send data to Parse.
9901054
json.put(KEY_COMPLETE, state.isComplete());
9911055
json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually);
1056+
JSONArray availableKeys = new JSONArray(state.availableKeys());
1057+
json.put(KEY_SELECTED_KEYS, availableKeys);
9921058

9931059
// Operation Set Queue
9941060
JSONArray operations = new JSONArray();
@@ -2872,7 +2938,7 @@ public void put(String key, Object value) {
28722938
if (value instanceof JSONObject) {
28732939
ParseDecoder decoder = ParseDecoder.get();
28742940
value = decoder.convertJSONObjectToMap((JSONObject) value);
2875-
} else if (value instanceof JSONArray){
2941+
} else if (value instanceof JSONArray) {
28762942
ParseDecoder decoder = ParseDecoder.get();
28772943
value = decoder.convertJSONArrayToList((JSONArray) value);
28782944
}
@@ -3036,6 +3102,7 @@ public boolean containsKey(String key) {
30363102
}
30373103
}
30383104

3105+
30393106
/**
30403107
* Access a {@link String} value.
30413108
*
@@ -3375,9 +3442,17 @@ public boolean isDataAvailable() {
33753442
}
33763443
}
33773444

3378-
/* package for tests */ boolean isDataAvailable(String key) {
3445+
/**
3446+
* Gets whether the {@code ParseObject} specified key has been fetched.
3447+
* This means the property can be accessed safely.
3448+
*
3449+
* @return {@code true} if the {@code ParseObject} key is new or has been fetched or refreshed. {@code false}
3450+
* otherwise.
3451+
*/
3452+
public boolean isDataAvailable(String key) {
33793453
synchronized (mutex) {
3380-
return isDataAvailable() || estimatedData.containsKey(key);
3454+
// Fallback to estimatedData to include dirty changes.
3455+
return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key);
33813456
}
33823457
}
33833458

Parse/src/test/java/com/parse/ParseDecoderTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static org.junit.Assert.assertNotNull;
2828
import static org.junit.Assert.assertNull;
2929
import static org.junit.Assert.assertSame;
30+
import static org.junit.Assert.assertTrue;
3031

3132
// For android.util.Base64
3233
@RunWith(RobolectricTestRunner.class)
@@ -198,6 +199,74 @@ public void testParseObject() throws JSONException {
198199
assertNotNull(parseObject);
199200
}
200201

202+
@Test
203+
public void testIncludedParseObject() throws JSONException {
204+
JSONObject json = new JSONObject();
205+
json.put("__type", "Object");
206+
json.put("className", "GameScore");
207+
json.put("createdAt", "2015-06-22T21:23:41.733Z");
208+
json.put("objectId", "TT1ZskATqS");
209+
json.put("updatedAt", "2015-06-22T22:06:18.104Z");
210+
211+
JSONObject child = new JSONObject();
212+
child.put("__type", "Object");
213+
child.put("className", "GameScore");
214+
child.put("createdAt", "2015-06-22T21:23:41.733Z");
215+
child.put("objectId", "TT1ZskATqR");
216+
child.put("updatedAt", "2015-06-22T22:06:18.104Z");
217+
218+
json.put("child", child);
219+
ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json);
220+
assertNotNull(parseObject.getParseObject("child"));
221+
}
222+
223+
@Test
224+
public void testCompleteness() throws JSONException {
225+
JSONObject json = new JSONObject();
226+
json.put("__type", "Object");
227+
json.put("className", "GameScore");
228+
json.put("createdAt", "2015-06-22T21:23:41.733Z");
229+
json.put("objectId", "TT1ZskATqS");
230+
json.put("updatedAt", "2015-06-22T22:06:18.104Z");
231+
json.put("foo", "foo");
232+
json.put("bar", "bar");
233+
ParseObject parseObject = (ParseObject) ParseDecoder.get().decode(json);
234+
assertTrue(parseObject.isDataAvailable());
235+
236+
JSONArray arr = new JSONArray("[\"foo\"]");
237+
json.put("__selectedKeys", arr);
238+
parseObject = (ParseObject) ParseDecoder.get().decode(json);
239+
assertFalse(parseObject.isDataAvailable());
240+
}
241+
242+
@Test
243+
public void testCompletenessOfIncludedParseObject() throws JSONException {
244+
JSONObject json = new JSONObject();
245+
json.put("__type", "Object");
246+
json.put("className", "GameScore");
247+
json.put("createdAt", "2015-06-22T21:23:41.733Z");
248+
json.put("objectId", "TT1ZskATqS");
249+
json.put("updatedAt", "2015-06-22T22:06:18.104Z");
250+
251+
JSONObject child = new JSONObject();
252+
child.put("__type", "Object");
253+
child.put("className", "GameScore");
254+
child.put("createdAt", "2015-06-22T21:23:41.733Z");
255+
child.put("objectId", "TT1ZskATqR");
256+
child.put("updatedAt", "2015-06-22T22:06:18.104Z");
257+
child.put("bar", "child bar");
258+
259+
JSONArray arr = new JSONArray("[\"foo.bar\"]");
260+
json.put("foo", child);
261+
json.put("__selectedKeys", arr);
262+
ParseObject parentObject = (ParseObject) ParseDecoder.get().decode(json);
263+
assertFalse(parentObject.isDataAvailable());
264+
assertTrue(parentObject.isDataAvailable("foo"));
265+
ParseObject childObject = parentObject.getParseObject("foo");
266+
assertFalse(childObject.isDataAvailable());
267+
assertTrue(childObject.isDataAvailable("bar"));
268+
}
269+
201270
@Test
202271
public void testRelation() throws JSONException {
203272
JSONObject json = new JSONObject();

0 commit comments

Comments
 (0)