@@ -64,6 +64,9 @@ public class ParseObject {
64
64
*/
65
65
private static final String KEY_COMPLETE = "__complete" ;
66
66
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" ;
67
70
/* package */ static final String KEY_IS_DELETING_EVENTUALLY = "__isDeletingEventually" ;
68
71
// Because Grantland messed up naming this... We'll only try to read from this for backward
69
72
// 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) {
98
101
private long createdAt = -1 ;
99
102
private long updatedAt = -1 ;
100
103
private boolean isComplete ;
104
+ private Set <String > availableKeys = new HashSet <>();
101
105
/* package */ Map <String , Object > serverData = new HashMap <>();
102
106
103
107
public Init (String className ) {
@@ -109,8 +113,10 @@ public Init(String className) {
109
113
objectId = state .objectId ();
110
114
createdAt = state .createdAt ();
111
115
updatedAt = state .updatedAt ();
116
+ availableKeys = state .availableKeys ();
112
117
for (String key : state .keySet ()) {
113
118
serverData .put (key , state .get (key ));
119
+ availableKeys .add (key );
114
120
}
115
121
isComplete = state .isComplete ();
116
122
}
@@ -151,6 +157,7 @@ public T isComplete(boolean complete) {
151
157
152
158
public T put (String key , Object value ) {
153
159
serverData .put (key , value );
160
+ availableKeys .add (key );
154
161
return self ();
155
162
}
156
163
@@ -159,12 +166,20 @@ public T remove(String key) {
159
166
return self ();
160
167
}
161
168
169
+ public T availableKeys (Collection <String > keys ) {
170
+ for (String key : keys ) {
171
+ availableKeys .add (key );
172
+ }
173
+ return self ();
174
+ }
175
+
162
176
public T clear () {
163
177
objectId = null ;
164
178
createdAt = -1 ;
165
179
updatedAt = -1 ;
166
180
isComplete = false ;
167
181
serverData .clear ();
182
+ availableKeys .clear ();
168
183
return self ();
169
184
}
170
185
@@ -188,6 +203,7 @@ public T apply(State other) {
188
203
for (String key : other .keySet ()) {
189
204
put (key , other .get (key ));
190
205
}
206
+ availableKeys (other .availableKeys ());
191
207
return self ();
192
208
}
193
209
@@ -231,6 +247,7 @@ public State build() {
231
247
private final long createdAt ;
232
248
private final long updatedAt ;
233
249
private final Map <String , Object > serverData ;
250
+ private final Set <String > availableKeys ;
234
251
private final boolean isComplete ;
235
252
236
253
/* package */ State (Init <?> builder ) {
@@ -242,6 +259,7 @@ public State build() {
242
259
: createdAt ;
243
260
serverData = Collections .unmodifiableMap (new HashMap <>(builder .serverData ));
244
261
isComplete = builder .isComplete ;
262
+ availableKeys = new HashSet <>(builder .availableKeys );
245
263
}
246
264
247
265
@ SuppressWarnings ("unchecked" )
@@ -277,19 +295,29 @@ public Set<String> keySet() {
277
295
return serverData .keySet ();
278
296
}
279
297
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
+
280
307
@ Override
281
308
public String toString () {
282
309
return String .format (Locale .US , "%s@%s[" +
283
310
"className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " +
284
- "serverData=%s]" ,
311
+ "serverData=%s, availableKeys=%s ]" ,
285
312
getClass ().getName (),
286
313
Integer .toHexString (hashCode ()),
287
314
className ,
288
315
objectId ,
289
316
createdAt ,
290
317
updatedAt ,
291
318
isComplete ,
292
- serverData );
319
+ serverData ,
320
+ availableKeys );
293
321
}
294
322
}
295
323
@@ -578,38 +606,48 @@ public Void then(Task<Void> task) throws Exception {
578
606
579
607
/**
580
608
* Creates a new {@code ParseObject} based on data from the Parse server.
581
- *
582
609
* @param json
583
610
* The object's data.
584
611
* @param defaultClassName
585
612
* 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.
588
618
*/
589
619
/* 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 );
592
631
}
593
632
594
633
/**
595
634
* Creates a new {@code ParseObject} based on data from the Parse server.
596
- *
597
635
* @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.
599
638
* @param defaultClassName
600
639
* 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.
603
640
* @param decoder
604
641
* Delegate for knowing how to decode the values in the JSON.
605
642
*/
606
643
/* package */ static <T extends ParseObject > T fromJSON (JSONObject json , String defaultClassName ,
607
- boolean isComplete , ParseDecoder decoder ) {
644
+ ParseDecoder decoder ) {
608
645
String className = json .optString (KEY_CLASS_NAME , defaultClassName );
609
646
if (className == null ) {
610
647
return null ;
611
648
}
612
649
String objectId = json .optString (KEY_OBJECT_ID , null );
650
+ boolean isComplete = !json .has (KEY_SELECTED_KEYS );
613
651
@ SuppressWarnings ("unchecked" )
614
652
T object = (T ) ParseObject .createWithoutData (className , objectId );
615
653
State newState = object .mergeFromServer (object .getState (), json , decoder , isComplete );
@@ -622,7 +660,7 @@ public Void then(Task<Void> task) throws Exception {
622
660
*
623
661
* Method is used by parse server webhooks implementation to create a
624
662
* 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
626
664
* {@link #build(JSONObject, ParseDecoder)} which populates operation queue
627
665
* rather then the server data from the incoming JSON, as at external server the incoming
628
666
* JSON may not represent the actual server data. Also it handles
@@ -876,9 +914,9 @@ protected boolean visit(Object object) {
876
914
}
877
915
}
878
916
917
+
879
918
/**
880
919
* Merges from JSON in REST format.
881
- *
882
920
* Updates this object with data from the server.
883
921
*
884
922
* @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder)
@@ -921,8 +959,34 @@ protected boolean visit(Object object) {
921
959
builder .put (KEY_ACL , acl );
922
960
continue ;
923
961
}
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
+ }
924
976
925
977
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
+ }
926
990
Object decodedObject = decoder .decode (value );
927
991
builder .put (key , decodedObject );
928
992
}
@@ -989,6 +1053,8 @@ protected boolean visit(Object object) {
989
1053
// using the REST api and want to send data to Parse.
990
1054
json .put (KEY_COMPLETE , state .isComplete ());
991
1055
json .put (KEY_IS_DELETING_EVENTUALLY , isDeletingEventually );
1056
+ JSONArray availableKeys = new JSONArray (state .availableKeys ());
1057
+ json .put (KEY_SELECTED_KEYS , availableKeys );
992
1058
993
1059
// Operation Set Queue
994
1060
JSONArray operations = new JSONArray ();
@@ -2872,7 +2938,7 @@ public void put(String key, Object value) {
2872
2938
if (value instanceof JSONObject ) {
2873
2939
ParseDecoder decoder = ParseDecoder .get ();
2874
2940
value = decoder .convertJSONObjectToMap ((JSONObject ) value );
2875
- } else if (value instanceof JSONArray ){
2941
+ } else if (value instanceof JSONArray ) {
2876
2942
ParseDecoder decoder = ParseDecoder .get ();
2877
2943
value = decoder .convertJSONArrayToList ((JSONArray ) value );
2878
2944
}
@@ -3036,6 +3102,7 @@ public boolean containsKey(String key) {
3036
3102
}
3037
3103
}
3038
3104
3105
+
3039
3106
/**
3040
3107
* Access a {@link String} value.
3041
3108
*
@@ -3375,9 +3442,17 @@ public boolean isDataAvailable() {
3375
3442
}
3376
3443
}
3377
3444
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 ) {
3379
3453
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 );
3381
3456
}
3382
3457
}
3383
3458
0 commit comments