Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding availableKeys to ParseObject.State #596

Merged
merged 10 commits into from
Mar 13, 2017
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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@natario1 do you remember why we need to pass in ParseDecoder.get() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rogerhu it’s just an instance of ParseDecoder.

I had to change the signatures of ParseObject.fromJSON and this seemed legit. ParseDecoder was present in the other signature (used here by ParseDecoder itself, passing this), so I added it here as well for consistency.

Since ParseDecoder is a singleton we could just drop it from both signatures, but I guess original authors wanted to keep the chance of passing a different decoder. I don’t know.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having to pass selected keys as metadata does it help to modify the signature in any way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we need a fromJSON signature that does not take selectedKeys() .. because ParseDecoder will just have a JSON and is not aware of the selected keys bundled in the JSON (I made KEY_SELECTED_KEYS private as you said), see.

From this point on, bundling the keys in the JSON allows us to not change all the other signatures, and abstract this internal stuff from ParseDecoder. It is also used in Local data store encoding/decoding, see ParseObject.toRest() / fromREST()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha forgot about local store too

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 @@ -2863,7 +2929,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 @@ -3027,6 +3093,7 @@ public boolean containsKey(String key) {
}
}


/**
* Access a {@link String} value.
*
Expand Down Expand Up @@ -3366,9 +3433,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