-
-
Notifications
You must be signed in to change notification settings - Fork 735
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
Changes from all commits
301ec93
2827522
b71193b
90877ae
f009d34
bc1c1fa
7d7cc45
6d2ccde
fcf24e3
b144ca0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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(); | ||
} | ||
|
@@ -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(); | ||
} | ||
|
||
|
@@ -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(); | ||
} | ||
|
||
|
@@ -188,6 +203,7 @@ public T apply(State other) { | |
for (String key : other.keySet()) { | ||
put(key, other.get(key)); | ||
} | ||
availableKeys(other.availableKeys()); | ||
return self(); | ||
} | ||
|
||
|
@@ -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) { | ||
|
@@ -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") | ||
|
@@ -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); | ||
} | ||
} | ||
|
||
|
@@ -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); | ||
|
@@ -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 | ||
|
@@ -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) | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha forgot about local store too |
||
builder.put(key, decodedObject); | ||
} | ||
|
@@ -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(); | ||
|
@@ -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); | ||
} | ||
|
@@ -3027,6 +3093,7 @@ public boolean containsKey(String key) { | |
} | ||
} | ||
|
||
|
||
/** | ||
* Access a {@link String} value. | ||
* | ||
|
@@ -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); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.