@@ -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
0 commit comments