Skip to content

Commit

Permalink
Adding availableKeys to ParseObject.State (#596)
Browse files Browse the repository at this point in the history
* 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()
  • Loading branch information
natario1 authored and rogerhu committed Mar 13, 2017
1 parent 809b829 commit ddf8de4
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Parse/src/main/java/com/parse/NetworkQueryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public Integer then(Task<JSONObject> task) throws Exception {
}
for (int i = 0; i < results.length(); ++i) {
JSONObject data = results.getJSONObject(i);
T object = ParseObject.fromJSON(data, resultClassName, state.selectedKeys() == null);
T object = ParseObject.fromJSON(data, resultClassName, ParseDecoder.get(), state.selectedKeys());
answer.add(object);

/*
Expand Down
2 changes: 1 addition & 1 deletion Parse/src/main/java/com/parse/ParseDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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, this);
}

if (typeString.equals("Relation")) {
Expand Down
109 changes: 92 additions & 17 deletions Parse/src/main/java/com/parse/ParseObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ 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.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
// compat, but I think we can be safe to assume any deleteEventuallys from long ago are obsolete
Expand Down Expand Up @@ -98,6 +101,7 @@ public static Init<?> newBuilder(String className) {
private long createdAt = -1;
private long updatedAt = -1;
private boolean isComplete;
private Set<String> availableKeys = new HashSet<>();
/* package */ Map<String, Object> serverData = new HashMap<>();

public Init(String className) {
Expand All @@ -109,8 +113,10 @@ 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);
}
isComplete = state.isComplete();
}
Expand Down Expand Up @@ -151,6 +157,7 @@ public T isComplete(boolean complete) {

public T put(String key, Object value) {
serverData.put(key, value);
availableKeys.add(key);
return self();
}

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

public T availableKeys(Collection<String> keys) {
for (String key : keys) {
availableKeys.add(key);
}
return self();
}

public T clear() {
objectId = null;
createdAt = -1;
updatedAt = -1;
isComplete = false;
serverData.clear();
availableKeys.clear();
return self();
}

Expand All @@ -188,6 +203,7 @@ public T apply(State other) {
for (String key : other.keySet()) {
put(key, other.get(key));
}
availableKeys(other.availableKeys());
return self();
}

Expand Down Expand Up @@ -231,6 +247,7 @@ public State build() {
private final long createdAt;
private final long updatedAt;
private final Map<String, Object> serverData;
private final Set<String> availableKeys;
private final boolean isComplete;

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

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -277,19 +295,29 @@ public Set<String> keySet() {
return serverData.keySet();
}

// 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<String> 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]",
"serverData=%s, availableKeys=%s]",
getClass().getName(),
Integer.toHexString(hashCode()),
className,
objectId,
createdAt,
updatedAt,
isComplete,
serverData);
serverData,
availableKeys);
}
}

Expand Down Expand Up @@ -578,38 +606,48 @@ public Void then(Task<Void> 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.
* @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 extends ParseObject> T fromJSON(JSONObject json, String defaultClassName,
boolean isComplete) {
return fromJSON(json, defaultClassName, isComplete, ParseDecoder.get());
ParseDecoder decoder,
Set<String> selectedKeys) {
if (selectedKeys != null && !selectedKeys.isEmpty()) {
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 extends ParseObject> 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);
Expand All @@ -622,7 +660,7 @@ public Void then(Task<Void> 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
Expand Down Expand Up @@ -876,9 +914,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)
Expand Down Expand Up @@ -921,8 +959,34 @@ protected boolean visit(Object object) {
builder.put(KEY_ACL, acl);
continue;
}
if (key.equals(KEY_SELECTED_KEYS)) {
JSONArray safeKeys = json.getJSONArray(key);
if (safeKeys.length() > 0) {
Collection<String> set = new HashSet<>();
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];
set.add(safeKey);
}
builder.availableKeys(set);
}
continue;
}

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);
}
Expand Down Expand Up @@ -989,6 +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 availableKeys = new JSONArray(state.availableKeys());
json.put(KEY_SELECTED_KEYS, availableKeys);

// Operation Set Queue
JSONArray operations = new JSONArray();
Expand Down Expand Up @@ -2872,7 +2938,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);
}
Expand Down Expand Up @@ -3036,6 +3102,7 @@ public boolean containsKey(String key) {
}
}


/**
* Access a {@link String} value.
*
Expand Down Expand Up @@ -3375,9 +3442,17 @@ 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);
// Fallback to estimatedData to include dirty changes.
return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key);
}
}

Expand Down
69 changes: 69 additions & 0 deletions Parse/src/test/java/com/parse/ParseDecoderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -198,6 +199,74 @@ 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 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();
Expand Down
Loading

0 comments on commit ddf8de4

Please sign in to comment.