4387 lines
151 KiB
Java
4387 lines
151 KiB
Java
/*
|
|
* Copyright (c) 2015-present, Parse, LLC.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
package com.parse;
|
|
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.util.Log;
|
|
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.ListIterator;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Callable;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.locks.Lock;
|
|
|
|
import bolts.Capture;
|
|
import bolts.Continuation;
|
|
import bolts.Task;
|
|
import bolts.TaskCompletionSource;
|
|
|
|
/**
|
|
* The {@code ParseObject} is a local representation of data that can be saved and retrieved from
|
|
* the Parse cloud.
|
|
* <p/>
|
|
* The basic workflow for creating new data is to construct a new {@code ParseObject}, use
|
|
* {@link #put(String, Object)} to fill it with data, and then use {@link #saveInBackground()} to
|
|
* persist to the cloud.
|
|
* <p/>
|
|
* The basic workflow for accessing existing data is to use a {@link ParseQuery} to specify which
|
|
* existing data to retrieve.
|
|
*/
|
|
public class ParseObject implements Parcelable {
|
|
private static final String AUTO_CLASS_NAME = "_Automatic";
|
|
/* package */ static final String VERSION_NAME = BuildConfig.VERSION_NAME;
|
|
private static final String TAG = "ParseObject";
|
|
|
|
/*
|
|
REST JSON Keys
|
|
*/
|
|
private static final String KEY_OBJECT_ID = "objectId";
|
|
private static final String KEY_CLASS_NAME = "className";
|
|
private static final String KEY_ACL = "ACL";
|
|
private static final String KEY_CREATED_AT = "createdAt";
|
|
private static final String KEY_UPDATED_AT = "updatedAt";
|
|
|
|
/*
|
|
Internal JSON Keys - Used to store internal data when persisting {@code ParseObject}s locally.
|
|
*/
|
|
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
|
|
// and not check after a while
|
|
private static final String KEY_IS_DELETING_EVENTUALLY_OLD = "isDeletingEventually";
|
|
|
|
private static ParseObjectController getObjectController() {
|
|
return ParseCorePlugins.getInstance().getObjectController();
|
|
}
|
|
|
|
private static LocalIdManager getLocalIdManager() {
|
|
return ParseCorePlugins.getInstance().getLocalIdManager();
|
|
}
|
|
|
|
private static ParseObjectSubclassingController getSubclassingController() {
|
|
return ParseCorePlugins.getInstance().getSubclassingController();
|
|
}
|
|
|
|
/** package */ static class State {
|
|
|
|
public static Init<?> newBuilder(String className) {
|
|
if ("_User".equals(className)) {
|
|
return new ParseUser.State.Builder();
|
|
}
|
|
return new Builder(className);
|
|
}
|
|
|
|
/* package */ static State createFromParcel(Parcel source, ParseParcelDecoder decoder) {
|
|
String className = source.readString();
|
|
if ("_User".equals(className)) {
|
|
return new ParseUser.State(source, className, decoder);
|
|
}
|
|
return new State(source, className, decoder);
|
|
}
|
|
|
|
/** package */ static abstract class Init<T extends Init> {
|
|
|
|
private final String className;
|
|
private String objectId;
|
|
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) {
|
|
this.className = className;
|
|
}
|
|
|
|
/* package */ Init(State state) {
|
|
className = state.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();
|
|
}
|
|
|
|
/* package */ abstract T self();
|
|
|
|
/* package */ abstract <S extends State> S build();
|
|
|
|
public T objectId(String objectId) {
|
|
this.objectId = objectId;
|
|
return self();
|
|
}
|
|
|
|
public T createdAt(Date createdAt) {
|
|
this.createdAt = createdAt.getTime();
|
|
return self();
|
|
}
|
|
|
|
public T createdAt(long createdAt) {
|
|
this.createdAt = createdAt;
|
|
return self();
|
|
}
|
|
|
|
public T updatedAt(Date updatedAt) {
|
|
this.updatedAt = updatedAt.getTime();
|
|
return self();
|
|
}
|
|
|
|
public T updatedAt(long updatedAt) {
|
|
this.updatedAt = updatedAt;
|
|
return self();
|
|
}
|
|
|
|
public T isComplete(boolean complete) {
|
|
isComplete = complete;
|
|
return self();
|
|
}
|
|
|
|
public T put(String key, Object value) {
|
|
serverData.put(key, value);
|
|
availableKeys.add(key);
|
|
return self();
|
|
}
|
|
|
|
public T remove(String key) {
|
|
serverData.remove(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();
|
|
}
|
|
|
|
/**
|
|
* Applies a {@code State} on top of this {@code Builder} instance.
|
|
*
|
|
* @param other The {@code State} to apply over this instance.
|
|
* @return A new {@code Builder} instance.
|
|
*/
|
|
public T apply(State other) {
|
|
if (other.objectId() != null) {
|
|
objectId(other.objectId());
|
|
}
|
|
if (other.createdAt() > 0) {
|
|
createdAt(other.createdAt());
|
|
}
|
|
if (other.updatedAt() > 0) {
|
|
updatedAt(other.updatedAt());
|
|
}
|
|
isComplete(isComplete || other.isComplete());
|
|
for (String key : other.keySet()) {
|
|
put(key, other.get(key));
|
|
}
|
|
availableKeys(other.availableKeys());
|
|
return self();
|
|
}
|
|
|
|
public T apply(ParseOperationSet operations) {
|
|
for (String key : operations.keySet()) {
|
|
ParseFieldOperation operation = operations.get(key);
|
|
Object oldValue = serverData.get(key);
|
|
Object newValue = operation.apply(oldValue, key);
|
|
if (newValue != null) {
|
|
put(key, newValue);
|
|
} else {
|
|
remove(key);
|
|
}
|
|
}
|
|
return self();
|
|
}
|
|
}
|
|
|
|
/* package */ static class Builder extends Init<Builder> {
|
|
|
|
public Builder(String className) {
|
|
super(className);
|
|
}
|
|
|
|
public Builder(State state) {
|
|
super(state);
|
|
}
|
|
|
|
@Override
|
|
/* package */ Builder self() {
|
|
return this;
|
|
}
|
|
|
|
public State build() {
|
|
return new State(this);
|
|
}
|
|
}
|
|
|
|
private final String className;
|
|
private final String objectId;
|
|
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) {
|
|
className = builder.className;
|
|
objectId = builder.objectId;
|
|
createdAt = builder.createdAt;
|
|
updatedAt = builder.updatedAt > 0
|
|
? builder.updatedAt
|
|
: createdAt;
|
|
serverData = Collections.unmodifiableMap(new HashMap<>(builder.serverData));
|
|
isComplete = builder.isComplete;
|
|
availableKeys = new HashSet<>(builder.availableKeys);
|
|
}
|
|
|
|
/* package */ State(Parcel parcel, String clazz, ParseParcelDecoder decoder) {
|
|
className = clazz; // Already read
|
|
objectId = parcel.readByte() == 1 ? parcel.readString() : null;
|
|
createdAt = parcel.readLong();
|
|
long updated = parcel.readLong();
|
|
updatedAt = updated > 0 ? updated : createdAt;
|
|
int size = parcel.readInt();
|
|
HashMap<String, Object> map = new HashMap<>();
|
|
for (int i = 0; i < size; i++) {
|
|
String key = parcel.readString();
|
|
Object obj = decoder.decode(parcel);
|
|
map.put(key, obj);
|
|
}
|
|
serverData = Collections.unmodifiableMap(map);
|
|
isComplete = parcel.readByte() == 1;
|
|
List<String> available = new ArrayList<>();
|
|
parcel.readStringList(available);
|
|
availableKeys = new HashSet<>(available);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public <T extends Init<?>> T newBuilder() {
|
|
return (T) new Builder(this);
|
|
}
|
|
|
|
public String className() {
|
|
return className;
|
|
}
|
|
|
|
public String objectId() {
|
|
return objectId;
|
|
}
|
|
|
|
public long createdAt() {
|
|
return createdAt;
|
|
}
|
|
|
|
public long updatedAt() {
|
|
return updatedAt;
|
|
}
|
|
|
|
public boolean isComplete() {
|
|
return isComplete;
|
|
}
|
|
|
|
public Object get(String key) {
|
|
return serverData.get(key);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
protected void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
|
|
dest.writeString(className);
|
|
dest.writeByte(objectId != null ? (byte) 1 : 0);
|
|
if (objectId != null) {
|
|
dest.writeString(objectId);
|
|
}
|
|
dest.writeLong(createdAt);
|
|
dest.writeLong(updatedAt);
|
|
dest.writeInt(serverData.size());
|
|
Set<String> keys = serverData.keySet();
|
|
for (String key : keys) {
|
|
dest.writeString(key);
|
|
encoder.encode(serverData.get(key), dest);
|
|
}
|
|
dest.writeByte(isComplete ? (byte) 1 : 0);
|
|
dest.writeStringList(new ArrayList<>(availableKeys));
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format(Locale.US, "%s@%s[" +
|
|
"className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " +
|
|
"serverData=%s, availableKeys=%s]",
|
|
getClass().getName(),
|
|
Integer.toHexString(hashCode()),
|
|
className,
|
|
objectId,
|
|
createdAt,
|
|
updatedAt,
|
|
isComplete,
|
|
serverData,
|
|
availableKeys);
|
|
}
|
|
}
|
|
|
|
/* package */ final Object mutex = new Object();
|
|
/* package */ final TaskQueue taskQueue = new TaskQueue();
|
|
|
|
private State state;
|
|
/* package */ final LinkedList<ParseOperationSet> operationSetQueue;
|
|
|
|
// Cached State
|
|
private final Map<String, Object> estimatedData;
|
|
|
|
/* package */ String localId;
|
|
private final ParseMulticastDelegate<ParseObject> saveEvent = new ParseMulticastDelegate<>();
|
|
|
|
/* package */ boolean isDeleted;
|
|
/* package */ boolean isDeleting; // Since delete ops are queued, we don't need a counter.
|
|
//TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count.
|
|
/* package */ int isDeletingEventually;
|
|
private boolean ldsEnabledWhenParceling;
|
|
|
|
private static final ThreadLocal<String> isCreatingPointerForObjectId =
|
|
new ThreadLocal<String>() {
|
|
@Override
|
|
protected String initialValue() {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* This is used only so that we can pass it to createWithoutData as the objectId to make it create
|
|
* an unfetched pointer that has no objectId. This is useful only in the context of the offline
|
|
* store, where you can have an unfetched pointer for an object that can later be fetched from the
|
|
* store.
|
|
*/
|
|
/* package */ private static final String NEW_OFFLINE_OBJECT_ID_PLACEHOLDER =
|
|
"*** Offline Object ***";
|
|
|
|
/**
|
|
* The base class constructor to call in subclasses. Uses the class name specified with the
|
|
* {@link ParseClassName} annotation on the subclass.
|
|
*/
|
|
protected ParseObject() {
|
|
this(AUTO_CLASS_NAME);
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code ParseObject} with no data in it. A {@code ParseObject} constructed in
|
|
* this way will not have an objectId and will not persist to the database until {@link #save()}
|
|
* is called.
|
|
* <p>
|
|
* Class names must be alphanumerical plus underscore, and start with a letter. It is recommended
|
|
* to name classes in <code>PascalCaseLikeThis</code>.
|
|
*
|
|
* @param theClassName
|
|
* The className for this {@code ParseObject}.
|
|
*/
|
|
public ParseObject(String theClassName) {
|
|
// We use a ThreadLocal rather than passing a parameter so that createWithoutData can do the
|
|
// right thing with subclasses. It's ugly and terrible, but it does provide the development
|
|
// experience we generally want, so... yeah. Sorry to whomever has to deal with this in the
|
|
// future. I pinky-swear we won't make a habit of this -- you believe me, don't you?
|
|
String objectIdForPointer = isCreatingPointerForObjectId.get();
|
|
|
|
if (theClassName == null) {
|
|
throw new IllegalArgumentException(
|
|
"You must specify a Parse class name when creating a new ParseObject.");
|
|
}
|
|
if (AUTO_CLASS_NAME.equals(theClassName)) {
|
|
theClassName = getSubclassingController().getClassName(getClass());
|
|
}
|
|
|
|
// If this is supposed to be created by a factory but wasn't, throw an exception.
|
|
if (!getSubclassingController().isSubclassValid(theClassName, getClass())) {
|
|
throw new IllegalArgumentException(
|
|
"You must create this type of ParseObject using ParseObject.create() or the proper subclass.");
|
|
}
|
|
|
|
operationSetQueue = new LinkedList<>();
|
|
operationSetQueue.add(new ParseOperationSet());
|
|
estimatedData = new HashMap<>();
|
|
|
|
State.Init<?> builder = newStateBuilder(theClassName);
|
|
// When called from new, assume hasData for the whole object is true.
|
|
if (objectIdForPointer == null) {
|
|
setDefaultValues();
|
|
builder.isComplete(true);
|
|
} else {
|
|
if (!objectIdForPointer.equals(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER)) {
|
|
builder.objectId(objectIdForPointer);
|
|
}
|
|
builder.isComplete(false);
|
|
}
|
|
// This is a new untouched object, we don't need cache rebuilding, etc.
|
|
state = builder.build();
|
|
|
|
OfflineStore store = Parse.getLocalDatastore();
|
|
if (store != null) {
|
|
store.registerNewObject(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@code ParseObject} based upon a class name. If the class name is a special type
|
|
* (e.g. for {@code ParseUser}), then the appropriate type of {@code ParseObject} is returned.
|
|
*
|
|
* @param className
|
|
* The class of object to create.
|
|
* @return A new {@code ParseObject} for the given class name.
|
|
*/
|
|
public static ParseObject create(String className) {
|
|
return getSubclassingController().newInstance(className);
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@code ParseObject} based upon a subclass type. Note that the object will be
|
|
* created based upon the {@link ParseClassName} of the given subclass type. For example, calling
|
|
* create(ParseUser.class) may create an instance of a custom subclass of {@code ParseUser}.
|
|
*
|
|
* @param subclass
|
|
* The class of object to create.
|
|
* @return A new {@code ParseObject} based upon the class name of the given subclass type.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
public static <T extends ParseObject> T create(Class<T> subclass) {
|
|
return (T) create(getSubclassingController().getClassName(subclass));
|
|
}
|
|
|
|
/**
|
|
* Creates a reference to an existing {@code ParseObject} for use in creating associations between
|
|
* {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return
|
|
* {@code false} until {@link #fetchIfNeeded()} or {@link #refresh()} has been called. No network
|
|
* request will be made.
|
|
*
|
|
* @param className
|
|
* The object's class.
|
|
* @param objectId
|
|
* The object id for the referenced object.
|
|
* @return A {@code ParseObject} without data.
|
|
*/
|
|
public static ParseObject createWithoutData(String className, String objectId) {
|
|
OfflineStore store = Parse.getLocalDatastore();
|
|
try {
|
|
if (objectId == null) {
|
|
isCreatingPointerForObjectId.set(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER);
|
|
} else {
|
|
isCreatingPointerForObjectId.set(objectId);
|
|
}
|
|
ParseObject object = null;
|
|
if (store != null && objectId != null) {
|
|
object = store.getObject(className, objectId);
|
|
}
|
|
|
|
if (object == null) {
|
|
object = create(className);
|
|
if (object.hasChanges()) {
|
|
throw new IllegalStateException(
|
|
"A ParseObject subclass default constructor must not make changes "
|
|
+ "to the object that cause it to be dirty."
|
|
);
|
|
}
|
|
}
|
|
|
|
return object;
|
|
|
|
} catch (RuntimeException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Failed to create instance of subclass.", e);
|
|
} finally {
|
|
isCreatingPointerForObjectId.set(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a reference to an existing {@code ParseObject} for use in creating associations between
|
|
* {@code ParseObject}s. Calling {@link #isDataAvailable()} on this object will return
|
|
* {@code false} until {@link #fetchIfNeeded()} or {@link #refresh()} has been called. No network
|
|
* request will be made.
|
|
*
|
|
* @param subclass
|
|
* The {@code ParseObject} subclass to create.
|
|
* @param objectId
|
|
* The object id for the referenced object.
|
|
* @return A {@code ParseObject} without data.
|
|
*/
|
|
@SuppressWarnings({"unused", "unchecked"})
|
|
public static <T extends ParseObject> T createWithoutData(Class<T> subclass, String objectId) {
|
|
return (T) createWithoutData(getSubclassingController().getClassName(subclass), objectId);
|
|
}
|
|
|
|
/**
|
|
* Registers a custom subclass type with the Parse SDK, enabling strong-typing of those
|
|
* {@code ParseObject}s whenever they appear. Subclasses must specify the {@link ParseClassName}
|
|
* annotation and have a default constructor.
|
|
*
|
|
* @param subclass
|
|
* The subclass type to register.
|
|
*/
|
|
public static void registerSubclass(Class<? extends ParseObject> subclass) {
|
|
getSubclassingController().registerSubclass(subclass);
|
|
}
|
|
|
|
/* package for tests */ static void unregisterSubclass(Class<? extends ParseObject> subclass) {
|
|
getSubclassingController().unregisterSubclass(subclass);
|
|
}
|
|
|
|
/**
|
|
* Adds a task to the queue for all of the given objects.
|
|
*/
|
|
static <T> Task<T> enqueueForAll(final List<? extends ParseObject> objects,
|
|
Continuation<Void, Task<T>> taskStart) {
|
|
// The task that will be complete when all of the child queues indicate they're ready to start.
|
|
final TaskCompletionSource<Void> readyToStart = new TaskCompletionSource<>();
|
|
|
|
// First, we need to lock the mutex for the queue for every object. We have to hold this
|
|
// from at least when taskStart() is called to when obj.taskQueue enqueue is called, so
|
|
// that saves actually get executed in the order they were setup by taskStart().
|
|
// The locks have to be sorted so that we always acquire them in the same order.
|
|
// Otherwise, there's some risk of deadlock.
|
|
List<Lock> locks = new ArrayList<>(objects.size());
|
|
for (ParseObject obj : objects) {
|
|
locks.add(obj.taskQueue.getLock());
|
|
}
|
|
LockSet lock = new LockSet(locks);
|
|
|
|
lock.lock();
|
|
try {
|
|
// The task produced by TaskStart
|
|
final Task<T> fullTask;
|
|
try {
|
|
// By running this immediately, we allow everything prior to toAwait to run before waiting
|
|
// for all of the queues on all of the objects.
|
|
fullTask = taskStart.then(readyToStart.getTask());
|
|
} catch (RuntimeException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
|
|
// Add fullTask to each of the objects' queues.
|
|
final List<Task<Void>> childTasks = new ArrayList<>();
|
|
for (ParseObject obj : objects) {
|
|
obj.taskQueue.enqueue(new Continuation<Void, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<Void> task) throws Exception {
|
|
childTasks.add(task);
|
|
return fullTask;
|
|
}
|
|
});
|
|
}
|
|
|
|
// When all of the objects' queues are ready, signal fullTask that it's ready to go on.
|
|
Task.whenAll(childTasks).continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
readyToStart.setResult(null);
|
|
return null;
|
|
}
|
|
});
|
|
return fullTask;
|
|
} finally {
|
|
lock.unlock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts a {@code ParseObject.State} to a {@code ParseObject}.
|
|
*
|
|
* @param state
|
|
* The {@code ParseObject.State} to convert from.
|
|
* @return A {@code ParseObject} instance.
|
|
*/
|
|
/* package */ static <T extends ParseObject> T from(ParseObject.State state) {
|
|
@SuppressWarnings("unchecked")
|
|
T object = (T) ParseObject.createWithoutData(state.className(), state.objectId());
|
|
synchronized (object.mutex) {
|
|
State newState;
|
|
if (state.isComplete()) {
|
|
newState = state;
|
|
} else {
|
|
newState = object.getState().newBuilder().apply(state).build();
|
|
}
|
|
object.setState(newState);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* 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 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,
|
|
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. 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 decoder
|
|
* Delegate for knowing how to decode the values in the JSON.
|
|
*/
|
|
/* package */ static <T extends ParseObject> T fromJSON(JSONObject json, String defaultClassName,
|
|
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);
|
|
object.setState(newState);
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* Method used by parse server webhooks implementation to convert raw JSON to Parse Object
|
|
*
|
|
* 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, 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
|
|
* {@link ParseFieldOperations} separately.
|
|
*
|
|
* @param json
|
|
* The object's data.
|
|
* @param decoder
|
|
* Delegate for knowing how to decode the values in the JSON.
|
|
*/
|
|
/* package */ static <T extends ParseObject> T fromJSONPayload(
|
|
JSONObject json, ParseDecoder decoder) {
|
|
String className = json.optString(KEY_CLASS_NAME);
|
|
if (className == null || ParseTextUtils.isEmpty(className)) {
|
|
return null;
|
|
}
|
|
String objectId = json.optString(KEY_OBJECT_ID, null);
|
|
@SuppressWarnings("unchecked")
|
|
T object = (T) ParseObject.createWithoutData(className, objectId);
|
|
object.build(json, decoder);
|
|
return object;
|
|
}
|
|
|
|
//region Getter/Setter helper methods
|
|
|
|
/* package */ State.Init<?> newStateBuilder(String className) {
|
|
return new State.Builder(className);
|
|
}
|
|
|
|
/* package */ State getState() {
|
|
synchronized (mutex) {
|
|
return state;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the current state of this object as well as updates our in memory cached state.
|
|
*
|
|
* @param newState The new state.
|
|
*/
|
|
/* package */ void setState(State newState) {
|
|
synchronized (mutex) {
|
|
setState(newState, true);
|
|
}
|
|
}
|
|
|
|
private void setState(State newState, boolean notifyIfObjectIdChanges) {
|
|
synchronized (mutex) {
|
|
String oldObjectId = state.objectId();
|
|
String newObjectId = newState.objectId();
|
|
|
|
state = newState;
|
|
|
|
if (notifyIfObjectIdChanges && !ParseTextUtils.equals(oldObjectId, newObjectId)) {
|
|
notifyObjectIdChanged(oldObjectId, newObjectId);
|
|
}
|
|
|
|
rebuildEstimatedData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accessor to the class name.
|
|
*/
|
|
public String getClassName() {
|
|
synchronized (mutex) {
|
|
return state.className();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This reports time as the server sees it, so that if you make changes to a {@code ParseObject}, then
|
|
* wait a while, and then call {@link #save()}, the updated time will be the time of the
|
|
* {@link #save()} call rather than the time the object was changed locally.
|
|
*
|
|
* @return The last time this object was updated on the server.
|
|
*/
|
|
public Date getUpdatedAt() {
|
|
long updatedAt = getState().updatedAt();
|
|
return updatedAt > 0
|
|
? new Date(updatedAt)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* This reports time as the server sees it, so that if you create a {@code ParseObject}, then wait a
|
|
* while, and then call {@link #save()}, the creation time will be the time of the first
|
|
* {@link #save()} call rather than the time the object was created locally.
|
|
*
|
|
* @return The first time this object was saved on the server.
|
|
*/
|
|
public Date getCreatedAt() {
|
|
long createdAt = getState().createdAt();
|
|
return createdAt > 0
|
|
? new Date(createdAt)
|
|
: null;
|
|
}
|
|
|
|
//endregion
|
|
|
|
/**
|
|
* Returns a set view of the keys contained in this object. This does not include createdAt,
|
|
* updatedAt, authData, or objectId. It does include things like username and ACL.
|
|
*/
|
|
public Set<String> keySet() {
|
|
synchronized (mutex) {
|
|
return Collections.unmodifiableSet(estimatedData.keySet());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies all of the operations that have been performed on another object since its last save
|
|
* onto this one.
|
|
*/
|
|
/* package */ void copyChangesFrom(ParseObject other) {
|
|
synchronized (mutex) {
|
|
ParseOperationSet operations = other.operationSetQueue.getFirst();
|
|
for (String key : operations.keySet()) {
|
|
performOperation(key, operations.get(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* package */ void mergeFromObject(ParseObject other) {
|
|
synchronized (mutex) {
|
|
// If they point to the same instance, we don't need to merge.
|
|
if (this == other) {
|
|
return;
|
|
}
|
|
|
|
State copy = other.getState().newBuilder().build();
|
|
|
|
// We don't want to notify if an objectId changed here since we utilize this method to merge
|
|
// an anonymous current user with a new ParseUser instance that's calling signUp(). This
|
|
// doesn't make any sense and we should probably remove that code in ParseUser.
|
|
// Otherwise, there shouldn't be any objectId changes here since this method is only otherwise
|
|
// used in fetchAll.
|
|
setState(copy, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears changes to this object's {@code key} made since the last call to {@link #save()} or
|
|
* {@link #saveInBackground()}.
|
|
*
|
|
* @param key The {@code key} to revert changes for.
|
|
*/
|
|
public void revert(String key) {
|
|
synchronized (mutex) {
|
|
if (isDirty(key)) {
|
|
currentOperations().remove(key);
|
|
rebuildEstimatedData();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears any changes to this object made since the last call to {@link #save()} or
|
|
* {@link #saveInBackground()}.
|
|
*/
|
|
public void revert() {
|
|
synchronized (mutex) {
|
|
if (isDirty()) {
|
|
currentOperations().clear();
|
|
rebuildEstimatedData();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deep traversal on this object to grab a copy of any object referenced by this object. These
|
|
* instances may have already been fetched, and we don't want to lose their data when refreshing
|
|
* or saving.
|
|
*
|
|
* @return the map mapping from objectId to {@code ParseObject} which has been fetched.
|
|
*/
|
|
private Map<String, ParseObject> collectFetchedObjects() {
|
|
final Map<String, ParseObject> fetchedObjects = new HashMap<>();
|
|
ParseTraverser traverser = new ParseTraverser() {
|
|
@Override
|
|
protected boolean visit(Object object) {
|
|
if (object instanceof ParseObject) {
|
|
ParseObject parseObj = (ParseObject) object;
|
|
State state = parseObj.getState();
|
|
if (state.objectId() != null && state.isComplete()) {
|
|
fetchedObjects.put(state.objectId(), parseObj);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
traverser.traverse(estimatedData);
|
|
return fetchedObjects;
|
|
}
|
|
|
|
/**
|
|
* Helper method called by {@link #fromJSONPayload(JSONObject, ParseDecoder)}
|
|
*
|
|
* The method helps webhooks implementation to build Parse object from raw JSON payload.
|
|
* It is different from {@link #mergeFromServer(State, JSONObject, ParseDecoder, boolean)}
|
|
* as the method saves the key value pairs (other than className, objectId, updatedAt and
|
|
* createdAt) in the operation queue rather than the server data. It also handles
|
|
* {@link ParseFieldOperations} differently.
|
|
*
|
|
* @param json : JSON object to be converted to Parse object
|
|
* @param decoder : Decoder to be used for Decoding JSON
|
|
*/
|
|
/* package */ void build(JSONObject json, ParseDecoder decoder) {
|
|
try {
|
|
State.Builder builder = new State.Builder(state)
|
|
.isComplete(true);
|
|
|
|
builder.clear();
|
|
|
|
Iterator<?> keys = json.keys();
|
|
while (keys.hasNext()) {
|
|
String key = (String) keys.next();
|
|
/*
|
|
__className: Used by fromJSONPayload, should be stripped out by the time it gets here...
|
|
*/
|
|
if (key.equals(KEY_CLASS_NAME)) {
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_OBJECT_ID)) {
|
|
String newObjectId = json.getString(key);
|
|
builder.objectId(newObjectId);
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_CREATED_AT)) {
|
|
builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key)));
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_UPDATED_AT)) {
|
|
builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key)));
|
|
continue;
|
|
}
|
|
|
|
Object value = json.get(key);
|
|
Object decodedObject = decoder.decode(value);
|
|
if (decodedObject instanceof ParseFieldOperation) {
|
|
performOperation(key, (ParseFieldOperation)decodedObject);
|
|
}
|
|
else {
|
|
put(key, decodedObject);
|
|
}
|
|
}
|
|
|
|
setState(builder.build());
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Merges from JSON in REST format.
|
|
* Updates this object with data from the server.
|
|
*
|
|
* @see #toJSONObjectForSaving(State, ParseOperationSet, ParseEncoder)
|
|
*/
|
|
/* package */ State mergeFromServer(
|
|
State state, JSONObject json, ParseDecoder decoder, boolean completeData) {
|
|
try {
|
|
// If server data is complete, consider this object to be fetched.
|
|
State.Init<?> builder = state.newBuilder();
|
|
if (completeData) {
|
|
builder.clear();
|
|
}
|
|
builder.isComplete(state.isComplete() || completeData);
|
|
|
|
Iterator<?> keys = json.keys();
|
|
while (keys.hasNext()) {
|
|
String key = (String) keys.next();
|
|
/*
|
|
__type: Returned by queries and cloud functions to designate body is a ParseObject
|
|
__className: Used by fromJSON, should be stripped out by the time it gets here...
|
|
*/
|
|
if (key.equals("__type") || key.equals(KEY_CLASS_NAME)) {
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_OBJECT_ID)) {
|
|
String newObjectId = json.getString(key);
|
|
builder.objectId(newObjectId);
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_CREATED_AT)) {
|
|
builder.createdAt(ParseDateFormat.getInstance().parse(json.getString(key)));
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_UPDATED_AT)) {
|
|
builder.updatedAt(ParseDateFormat.getInstance().parse(json.getString(key)));
|
|
continue;
|
|
}
|
|
if (key.equals(KEY_ACL)) {
|
|
ParseACL acl = ParseACL.createACLFromJSONObject(json.getJSONObject(key), decoder);
|
|
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);
|
|
}
|
|
|
|
return builder.build();
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
//region LDS-processing methods.
|
|
|
|
/**
|
|
* Convert to REST JSON for persisting in LDS.
|
|
*
|
|
* @see #mergeREST(State, org.json.JSONObject, ParseDecoder)
|
|
*/
|
|
/* package */ JSONObject toRest(ParseEncoder encoder) {
|
|
State state;
|
|
List<ParseOperationSet> operationSetQueueCopy;
|
|
synchronized (mutex) {
|
|
// mutex needed to lock access to state and operationSetQueue and operationSetQueue & children
|
|
// are mutable
|
|
state = getState();
|
|
|
|
// operationSetQueue is a List of Lists, so we'll need custom copying logic
|
|
int operationSetQueueSize = operationSetQueue.size();
|
|
operationSetQueueCopy = new ArrayList<>(operationSetQueueSize);
|
|
for (int i = 0; i < operationSetQueueSize; i++) {
|
|
ParseOperationSet original = operationSetQueue.get(i);
|
|
ParseOperationSet copy = new ParseOperationSet(original);
|
|
operationSetQueueCopy.add(copy);
|
|
}
|
|
}
|
|
return toRest(state, operationSetQueueCopy, encoder);
|
|
}
|
|
|
|
/* package */ JSONObject toRest(
|
|
State state, List<ParseOperationSet> operationSetQueue, ParseEncoder objectEncoder) {
|
|
// Public data goes in dataJSON; special fields go in objectJSON.
|
|
JSONObject json = new JSONObject();
|
|
|
|
try {
|
|
// REST JSON (State)
|
|
json.put(KEY_CLASS_NAME, state.className());
|
|
if (state.objectId() != null) {
|
|
json.put(KEY_OBJECT_ID, state.objectId());
|
|
}
|
|
if (state.createdAt() > 0) {
|
|
json.put(KEY_CREATED_AT,
|
|
ParseDateFormat.getInstance().format(new Date(state.createdAt())));
|
|
}
|
|
if (state.updatedAt() > 0) {
|
|
json.put(KEY_UPDATED_AT,
|
|
ParseDateFormat.getInstance().format(new Date(state.updatedAt())));
|
|
}
|
|
for (String key : state.keySet()) {
|
|
Object value = state.get(key);
|
|
json.put(key, objectEncoder.encode(value));
|
|
}
|
|
|
|
// Internal JSON
|
|
//TODO(klimt): We'll need to rip all this stuff out and put it somewhere else if we start
|
|
// 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();
|
|
for (ParseOperationSet operationSet : operationSetQueue) {
|
|
operations.put(operationSet.toRest(objectEncoder));
|
|
}
|
|
json.put(KEY_OPERATIONS, operations);
|
|
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException("could not serialize object to JSON");
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
/**
|
|
* Merge with REST JSON from LDS.
|
|
*
|
|
* @see #toRest(ParseEncoder)
|
|
*/
|
|
/* package */ void mergeREST(State state, JSONObject json, ParseDecoder decoder) {
|
|
ArrayList<ParseOperationSet> saveEventuallyOperationSets = new ArrayList<>();
|
|
|
|
synchronized (mutex) {
|
|
try {
|
|
boolean isComplete = json.getBoolean(KEY_COMPLETE);
|
|
isDeletingEventually = ParseJSONUtils.getInt(json, Arrays.asList(
|
|
KEY_IS_DELETING_EVENTUALLY,
|
|
KEY_IS_DELETING_EVENTUALLY_OLD
|
|
));
|
|
JSONArray operations = json.getJSONArray(KEY_OPERATIONS);
|
|
{
|
|
ParseOperationSet newerOperations = currentOperations();
|
|
operationSetQueue.clear();
|
|
|
|
// Add and enqueue any saveEventually operations, roll forward any other operation sets
|
|
// (operation sets here are generally failed/incomplete saves).
|
|
ParseOperationSet current = null;
|
|
for (int i = 0; i < operations.length(); i++) {
|
|
JSONObject operationSetJSON = operations.getJSONObject(i);
|
|
ParseOperationSet operationSet = ParseOperationSet.fromRest(operationSetJSON, decoder);
|
|
|
|
if (operationSet.isSaveEventually()) {
|
|
if (current != null) {
|
|
operationSetQueue.add(current);
|
|
current = null;
|
|
}
|
|
saveEventuallyOperationSets.add(operationSet);
|
|
operationSetQueue.add(operationSet);
|
|
continue;
|
|
}
|
|
|
|
if (current != null) {
|
|
operationSet.mergeFrom(current);
|
|
}
|
|
current = operationSet;
|
|
}
|
|
if (current != null) {
|
|
operationSetQueue.add(current);
|
|
}
|
|
|
|
// Merge the changes that were previously in memory into the updated object.
|
|
currentOperations().mergeFrom(newerOperations);
|
|
}
|
|
|
|
// We only want to merge server data if we our updatedAt is null (we're unsaved or from
|
|
// #createWithoutData) or if the JSON's updatedAt is newer than ours.
|
|
boolean mergeServerData = false;
|
|
if (state.updatedAt() < 0) {
|
|
mergeServerData = true;
|
|
} else if (json.has(KEY_UPDATED_AT)) {
|
|
Date otherUpdatedAt = ParseDateFormat.getInstance().parse(json.getString(KEY_UPDATED_AT));
|
|
if (new Date(state.updatedAt()).compareTo(otherUpdatedAt) < 0) {
|
|
mergeServerData = true;
|
|
}
|
|
}
|
|
|
|
if (mergeServerData) {
|
|
// Clean up internal json keys
|
|
JSONObject mergeJSON = ParseJSONUtils.create(json, Arrays.asList(
|
|
KEY_COMPLETE, KEY_IS_DELETING_EVENTUALLY, KEY_IS_DELETING_EVENTUALLY_OLD,
|
|
KEY_OPERATIONS
|
|
));
|
|
State newState = mergeFromServer(state, mergeJSON, decoder, isComplete);
|
|
setState(newState);
|
|
}
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
// We cannot modify the taskQueue inside synchronized (mutex).
|
|
for (ParseOperationSet operationSet : saveEventuallyOperationSets) {
|
|
enqueueSaveEventuallyOperationAsync(operationSet);
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
|
|
private boolean hasDirtyChildren() {
|
|
synchronized (mutex) {
|
|
// We only need to consider the currently estimated children here,
|
|
// because they're the only ones that might need to be saved in a
|
|
// subsequent call to save, which is the meaning of "dirtiness".
|
|
List<ParseObject> unsavedChildren = new ArrayList<>();
|
|
collectDirtyChildren(estimatedData, unsavedChildren, null);
|
|
return unsavedChildren.size() > 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether any key-value pair in this object (or its children) has been added/updated/removed and
|
|
* not saved yet.
|
|
*
|
|
* @return Whether this object has been altered and not saved yet.
|
|
*/
|
|
public boolean isDirty() {
|
|
return this.isDirty(true);
|
|
}
|
|
|
|
/* package */ boolean isDirty(boolean considerChildren) {
|
|
synchronized (mutex) {
|
|
return (isDeleted || getObjectId() == null || hasChanges() || (considerChildren && hasDirtyChildren()));
|
|
}
|
|
}
|
|
|
|
boolean hasChanges() {
|
|
synchronized (mutex) {
|
|
return currentOperations().size() > 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this {@code ParseObject} has operations in operationSetQueue that
|
|
* haven't been completed yet, {@code false} if there are no operations in the operationSetQueue.
|
|
*/
|
|
/* package */ boolean hasOutstandingOperations() {
|
|
synchronized (mutex) {
|
|
// > 1 since 1 is for unsaved changes.
|
|
return operationSetQueue.size() > 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether a value associated with a key has been added/updated/removed and not saved yet.
|
|
*
|
|
* @param key
|
|
* The key to check for
|
|
* @return Whether this key has been altered and not saved yet.
|
|
*/
|
|
public boolean isDirty(String key) {
|
|
synchronized (mutex) {
|
|
return currentOperations().containsKey(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accessor to the object id. An object id is assigned as soon as an object is saved to the
|
|
* server. The combination of a className and an objectId uniquely identifies an object in your
|
|
* application.
|
|
*
|
|
* @return The object id.
|
|
*/
|
|
public String getObjectId() {
|
|
synchronized (mutex) {
|
|
return state.objectId();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setter for the object id. In general you do not need to use this. However, in some cases this
|
|
* can be convenient. For example, if you are serializing a {@code ParseObject} yourself and wish
|
|
* to recreate it, you can use this to recreate the {@code ParseObject} exactly.
|
|
*/
|
|
public void setObjectId(String newObjectId) {
|
|
synchronized (mutex) {
|
|
String oldObjectId = state.objectId();
|
|
if (ParseTextUtils.equals(oldObjectId, newObjectId)) {
|
|
return;
|
|
}
|
|
|
|
// We don't need to use setState since it doesn't affect our cached state.
|
|
state = state.newBuilder().objectId(newObjectId).build();
|
|
notifyObjectIdChanged(oldObjectId, newObjectId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the localId, which is used internally for serializing relations to objects that don't
|
|
* yet have an objectId.
|
|
*/
|
|
/* package */ String getOrCreateLocalId() {
|
|
synchronized (mutex) {
|
|
if (localId == null) {
|
|
if (state.objectId() != null) {
|
|
throw new IllegalStateException(
|
|
"Attempted to get a localId for an object with an objectId.");
|
|
}
|
|
localId = getLocalIdManager().createLocalId();
|
|
}
|
|
return localId;
|
|
}
|
|
}
|
|
|
|
// Sets the objectId without marking dirty.
|
|
private void notifyObjectIdChanged(String oldObjectId, String newObjectId) {
|
|
synchronized (mutex) {
|
|
// The offline store may throw if this object already had a different objectId.
|
|
OfflineStore store = Parse.getLocalDatastore();
|
|
if (store != null) {
|
|
store.updateObjectId(this, oldObjectId, newObjectId);
|
|
}
|
|
|
|
if (localId != null) {
|
|
getLocalIdManager().setObjectId(localId, newObjectId);
|
|
localId = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ParseRESTObjectCommand currentSaveEventuallyCommand(
|
|
ParseOperationSet operations, ParseEncoder objectEncoder, String sessionToken)
|
|
throws ParseException {
|
|
State state = getState();
|
|
|
|
/*
|
|
* Get the JSON representation of the object, and use some of the information to construct the
|
|
* command.
|
|
*/
|
|
JSONObject objectJSON = toJSONObjectForSaving(state, operations, objectEncoder);
|
|
|
|
ParseRESTObjectCommand command = ParseRESTObjectCommand.saveObjectCommand(
|
|
state,
|
|
objectJSON,
|
|
sessionToken);
|
|
return command;
|
|
}
|
|
|
|
/**
|
|
* Converts a {@code ParseObject} to a JSON representation for saving to Parse.
|
|
*
|
|
* <pre>
|
|
* {
|
|
* data: { // objectId plus any ParseFieldOperations },
|
|
* classname: class name for the object
|
|
* }
|
|
* </pre>
|
|
*
|
|
* updatedAt and createdAt are not included. only dirty keys are represented in the data.
|
|
*
|
|
* @see #mergeFromServer(State state, org.json.JSONObject, ParseDecoder, boolean)
|
|
*/
|
|
// Currently only used by saveEventually
|
|
/* package */ <T extends State> JSONObject toJSONObjectForSaving(
|
|
T state, ParseOperationSet operations, ParseEncoder objectEncoder) {
|
|
JSONObject objectJSON = new JSONObject();
|
|
|
|
try {
|
|
// Serialize the data
|
|
for (String key : operations.keySet()) {
|
|
ParseFieldOperation operation = operations.get(key);
|
|
objectJSON.put(key, objectEncoder.encode(operation));
|
|
|
|
// TODO(grantland): Use cached value from hashedObjects if it's a set operation.
|
|
}
|
|
|
|
if (state.objectId() != null) {
|
|
objectJSON.put(KEY_OBJECT_ID, state.objectId());
|
|
}
|
|
} catch (JSONException e) {
|
|
throw new RuntimeException("could not serialize object to JSON");
|
|
}
|
|
|
|
return objectJSON;
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code save}.
|
|
*
|
|
* Should be called on success or failure.
|
|
*/
|
|
// TODO(grantland): Remove once we convert saveEventually and ParseUser.signUp/resolveLaziness
|
|
// to controllers
|
|
/* package */ Task<Void> handleSaveResultAsync(
|
|
final JSONObject result, final ParseOperationSet operationsBeforeSave) {
|
|
ParseObject.State newState = null;
|
|
|
|
if (result != null) { // Success
|
|
synchronized (mutex) {
|
|
final Map<String, ParseObject> fetchedObjects = collectFetchedObjects();
|
|
ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects);
|
|
newState = ParseObjectCoder.get().decode(getState().newBuilder().clear(), result, decoder)
|
|
.isComplete(false)
|
|
.build();
|
|
}
|
|
}
|
|
|
|
return handleSaveResultAsync(newState, operationsBeforeSave);
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code save}.
|
|
*
|
|
* Should be called on success or failure.
|
|
*/
|
|
/* package */ Task<Void> handleSaveResultAsync(
|
|
final ParseObject.State result, final ParseOperationSet operationsBeforeSave) {
|
|
Task<Void> task = Task.forResult(null);
|
|
|
|
final boolean success = result != null;
|
|
synchronized (mutex) {
|
|
// Find operationsBeforeSave in the queue so that we can remove it and move to the next
|
|
// operation set.
|
|
ListIterator<ParseOperationSet> opIterator =
|
|
operationSetQueue.listIterator(operationSetQueue.indexOf(operationsBeforeSave));
|
|
opIterator.next();
|
|
opIterator.remove();
|
|
|
|
if (!success) {
|
|
// Merge the data from the failed save into the next save.
|
|
ParseOperationSet nextOperation = opIterator.next();
|
|
nextOperation.mergeFrom(operationsBeforeSave);
|
|
return task;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If this object is in the offline store, then we need to make sure that we pull in any dirty
|
|
* changes it may have before merging the server data into it.
|
|
*/
|
|
final OfflineStore store = Parse.getLocalDatastore();
|
|
if (store != null) {
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return store.fetchLocallyAsync(ParseObject.this).makeVoid();
|
|
}
|
|
});
|
|
}
|
|
|
|
// fetchLocallyAsync will return an error if this object isn't in the LDS yet and that's ok
|
|
task = task.continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
synchronized (mutex) {
|
|
State newState;
|
|
if (result.isComplete()) {
|
|
// Result is complete, so just replace
|
|
newState = result;
|
|
} else {
|
|
// Result is incomplete, so we'll need to apply it to the current state
|
|
newState = getState().newBuilder()
|
|
.apply(operationsBeforeSave)
|
|
.apply(result)
|
|
.build();
|
|
}
|
|
setState(newState);
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (store != null) {
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return store.updateDataForObjectAsync(ParseObject.this);
|
|
}
|
|
});
|
|
}
|
|
|
|
task = task.onSuccess(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
saveEvent.invoke(ParseObject.this, null);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
return task;
|
|
}
|
|
|
|
/* package */ ParseOperationSet startSave() {
|
|
synchronized (mutex) {
|
|
ParseOperationSet currentOperations = currentOperations();
|
|
operationSetQueue.addLast(new ParseOperationSet());
|
|
return currentOperations;
|
|
}
|
|
}
|
|
|
|
/* package */ void validateSave() {
|
|
// do nothing
|
|
}
|
|
|
|
/**
|
|
* Saves this object to the server. Typically, you should use {@link #saveInBackground} instead of
|
|
* this, unless you are managing your own threading.
|
|
*
|
|
* @throws ParseException
|
|
* Throws an exception if the server is inaccessible.
|
|
*/
|
|
public final void save() throws ParseException {
|
|
ParseTaskUtils.wait(saveInBackground());
|
|
}
|
|
|
|
/**
|
|
* Saves this object to the server in a background thread. This is preferable to using {@link #save()},
|
|
* unless your code is already running from a background thread.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when the save completes.
|
|
*/
|
|
public final Task<Void> saveInBackground() {
|
|
return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation<ParseUser, Task<String>>() {
|
|
@Override
|
|
public Task<String> then(Task<ParseUser> task) throws Exception {
|
|
final ParseUser current = task.getResult();
|
|
if (current == null) {
|
|
return Task.forResult(null);
|
|
}
|
|
if (!current.isLazy()) {
|
|
return Task.forResult(current.getSessionToken());
|
|
}
|
|
|
|
// The current user is lazy/unresolved. If it is attached to us via ACL, we'll need to
|
|
// resolve/save it before proceeding.
|
|
if (!isDataAvailable(KEY_ACL)) {
|
|
return Task.forResult(null);
|
|
}
|
|
final ParseACL acl = getACL(false);
|
|
if (acl == null) {
|
|
return Task.forResult(null);
|
|
}
|
|
final ParseUser user = acl.getUnresolvedUser();
|
|
if (user == null || !user.isCurrentUser()) {
|
|
return Task.forResult(null);
|
|
}
|
|
return user.saveAsync(null).onSuccess(new Continuation<Void, String>() {
|
|
@Override
|
|
public String then(Task<Void> task) throws Exception {
|
|
if (acl.hasUnresolvedUser()) {
|
|
throw new IllegalStateException("ACL has an unresolved ParseUser. "
|
|
+ "Save or sign up before attempting to serialize the ACL.");
|
|
}
|
|
return user.getSessionToken();
|
|
}
|
|
});
|
|
}
|
|
}).onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
final String sessionToken = task.getResult();
|
|
return saveAsync(sessionToken);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ Task<Void> saveAsync(final String sessionToken) {
|
|
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
return saveAsync(sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* package */ Task<Void> saveAsync(final String sessionToken, final Task<Void> toAwait) {
|
|
if (!isDirty()) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
final ParseOperationSet operations;
|
|
synchronized (mutex) {
|
|
updateBeforeSave();
|
|
validateSave();
|
|
operations = startSave();
|
|
}
|
|
|
|
Task<Void> task;
|
|
synchronized (mutex) {
|
|
// Recursively save children
|
|
|
|
/*
|
|
* TODO(klimt): Why is this estimatedData and not... I mean, what if a child is
|
|
* removed after save is called, but before the unresolved user gets resolved? It
|
|
* won't get saved.
|
|
*/
|
|
task = deepSaveAsync(estimatedData, sessionToken);
|
|
}
|
|
|
|
return task.onSuccessTask(
|
|
TaskQueue.<Void>waitFor(toAwait)
|
|
).onSuccessTask(new Continuation<Void, Task<ParseObject.State>>() {
|
|
@Override
|
|
public Task<ParseObject.State> then(Task<Void> task) throws Exception {
|
|
final Map<String, ParseObject> fetchedObjects = collectFetchedObjects();
|
|
ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects);
|
|
return getObjectController().saveAsync(getState(), operations, sessionToken, decoder);
|
|
}
|
|
}).continueWithTask(new Continuation<ParseObject.State, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(final Task<ParseObject.State> saveTask) throws Exception {
|
|
ParseObject.State result = saveTask.getResult();
|
|
return handleSaveResultAsync(result, operations).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
if (task.isFaulted() || task.isCancelled()) {
|
|
return task;
|
|
}
|
|
|
|
// We still want to propagate saveTask errors
|
|
return saveTask.makeVoid();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Currently only used by ParsePinningEventuallyQueue for saveEventually due to the limitation in
|
|
// ParseCommandCache that it can only return JSONObject result.
|
|
/* package */ Task<JSONObject> saveAsync(
|
|
ParseHttpClient client,
|
|
final ParseOperationSet operationSet,
|
|
String sessionToken) throws ParseException {
|
|
final ParseRESTCommand command =
|
|
currentSaveEventuallyCommand(operationSet, PointerEncoder.get(), sessionToken);
|
|
return command.executeAsync(client);
|
|
}
|
|
|
|
/**
|
|
* Saves this object to the server in a background thread. This is preferable to using {@link #save()},
|
|
* unless your code is already running from a background thread.
|
|
*
|
|
* @param callback
|
|
* {@code callback.done(e)} is called when the save completes.
|
|
*/
|
|
public final void saveInBackground(SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback);
|
|
}
|
|
|
|
/* package */ void validateSaveEventually() throws ParseException {
|
|
// do nothing
|
|
}
|
|
|
|
/**
|
|
* Saves this object to the server at some unspecified time in the future, even if Parse is
|
|
* currently inaccessible. Use this when you may not have a solid network connection, and don't
|
|
* need to know when the save completes. If there is some problem with the object such that it
|
|
* can't be saved, it will be silently discarded. Objects saved with this method will be stored
|
|
* locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately
|
|
* if possible. Otherwise, they will be sent the next time a network connection is available.
|
|
* Objects saved this way will persist even after the app is closed, in which case they will be
|
|
* sent the next time the app is opened. If more than 10MB of data is waiting to be sent,
|
|
* subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old
|
|
* saves to be silently discarded until the connection can be re-established, and the queued
|
|
* objects can be saved.
|
|
*
|
|
* @param callback
|
|
* - A callback which will be called if the save completes before the app exits.
|
|
*/
|
|
public final void saveEventually(SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(saveEventually(), callback);
|
|
}
|
|
|
|
/**
|
|
* Saves this object to the server at some unspecified time in the future, even if Parse is
|
|
* currently inaccessible. Use this when you may not have a solid network connection, and don't
|
|
* need to know when the save completes. If there is some problem with the object such that it
|
|
* can't be saved, it will be silently discarded. Objects saved with this method will be stored
|
|
* locally in an on-disk cache until they can be delivered to Parse. They will be sent immediately
|
|
* if possible. Otherwise, they will be sent the next time a network connection is available.
|
|
* Objects saved this way will persist even after the app is closed, in which case they will be
|
|
* sent the next time the app is opened. If more than 10MB of data is waiting to be sent,
|
|
* subsequent calls to {@code #saveEventually()} or {@link #deleteEventually()} will cause old
|
|
* saves to be silently discarded until the connection can be re-established, and the queued
|
|
* objects can be saved.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when the save completes.
|
|
*/
|
|
public final Task<Void> saveEventually() {
|
|
if (!isDirty()) {
|
|
Parse.getEventuallyQueue().fakeObjectUpdate();
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
final ParseOperationSet operationSet;
|
|
final ParseRESTCommand command;
|
|
final Task<JSONObject> runEventuallyTask;
|
|
|
|
synchronized (mutex) {
|
|
updateBeforeSave();
|
|
try {
|
|
validateSaveEventually();
|
|
} catch (ParseException e) {
|
|
return Task.forError(e);
|
|
}
|
|
|
|
// TODO(klimt): Once we allow multiple saves on an object, this
|
|
// should be collecting dirty children from the estimate based on
|
|
// whatever data is going to be sent by this saveEventually, which
|
|
// won't necessarily be the current estimatedData. We should resolve
|
|
// this when the multiple save code is added.
|
|
List<ParseObject> unsavedChildren = new ArrayList<>();
|
|
collectDirtyChildren(estimatedData, unsavedChildren, null);
|
|
|
|
String localId = null;
|
|
if (getObjectId() == null) {
|
|
localId = getOrCreateLocalId();
|
|
}
|
|
|
|
operationSet = startSave();
|
|
operationSet.setIsSaveEventually(true);
|
|
|
|
//TODO (grantland): Convert to async
|
|
final String sessionToken = ParseUser.getCurrentSessionToken();
|
|
|
|
try {
|
|
// See [1]
|
|
command = currentSaveEventuallyCommand(operationSet, PointerOrLocalIdEncoder.get(),
|
|
sessionToken);
|
|
|
|
// TODO: Make this logic make sense once we have deepSaveEventually
|
|
command.setLocalId(localId);
|
|
|
|
// Mark the command with a UUID so that we can match it up later.
|
|
command.setOperationSetUUID(operationSet.getUUID());
|
|
|
|
// Ensure local ids are retained before saveEventually-ing children
|
|
command.retainLocalIds();
|
|
|
|
for (ParseObject object : unsavedChildren) {
|
|
object.saveEventually();
|
|
}
|
|
} catch (ParseException exception) {
|
|
throw new IllegalStateException("Unable to saveEventually.", exception);
|
|
}
|
|
}
|
|
|
|
// We cannot modify the taskQueue inside synchronized (mutex).
|
|
ParseEventuallyQueue cache = Parse.getEventuallyQueue();
|
|
runEventuallyTask = cache.enqueueEventuallyAsync(command, ParseObject.this);
|
|
enqueueSaveEventuallyOperationAsync(operationSet);
|
|
|
|
// Release the extra retained local ids.
|
|
command.releaseLocalIds();
|
|
|
|
Task<Void> handleSaveResultTask;
|
|
if (Parse.isLocalDatastoreEnabled()) {
|
|
// ParsePinningEventuallyQueue calls handleSaveEventuallyResultAsync directly.
|
|
handleSaveResultTask = runEventuallyTask.makeVoid();
|
|
} else {
|
|
handleSaveResultTask = runEventuallyTask.onSuccessTask(new Continuation<JSONObject, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<JSONObject> task) throws Exception {
|
|
JSONObject json = task.getResult();
|
|
return handleSaveEventuallyResultAsync(json, operationSet);
|
|
}
|
|
});
|
|
}
|
|
return handleSaveResultTask;
|
|
}
|
|
|
|
/**
|
|
* Enqueues the saveEventually ParseOperationSet in {@link #taskQueue}.
|
|
*/
|
|
private Task<Void> enqueueSaveEventuallyOperationAsync(final ParseOperationSet operationSet) {
|
|
if (!operationSet.isSaveEventually()) {
|
|
throw new IllegalStateException(
|
|
"This should only be used to enqueue saveEventually operation sets");
|
|
}
|
|
|
|
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
ParseEventuallyQueue cache = Parse.getEventuallyQueue();
|
|
return cache.waitForOperationSetAndEventuallyPin(operationSet, null).makeVoid();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code saveEventually}.
|
|
*
|
|
* In addition to normal save handling, this also notifies the saveEventually test helper.
|
|
*
|
|
* Should be called on success or failure.
|
|
*/
|
|
/* package */ Task<Void> handleSaveEventuallyResultAsync(
|
|
JSONObject json, ParseOperationSet operationSet) {
|
|
final boolean success = json != null;
|
|
Task<Void> handleSaveResultTask = handleSaveResultAsync(json, operationSet);
|
|
|
|
return handleSaveResultTask.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
if (success) {
|
|
Parse.getEventuallyQueue()
|
|
.notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_UPDATED);
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called by {@link #saveInBackground()} and {@link #saveEventually(SaveCallback)}
|
|
* and guaranteed to be thread-safe. Subclasses can override this method to do any custom updates
|
|
* before an object gets saved.
|
|
*/
|
|
/* package */ void updateBeforeSave() {
|
|
// do nothing
|
|
}
|
|
|
|
/**
|
|
* Deletes this object from the server at some unspecified time in the future, even if Parse is
|
|
* currently inaccessible. Use this when you may not have a solid network connection, and don't
|
|
* need to know when the delete completes. If there is some problem with the object such that it
|
|
* can't be deleted, the request will be silently discarded. Delete requests made with this method
|
|
* will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be
|
|
* sent immediately if possible. Otherwise, they will be sent the next time a network connection
|
|
* is available. Delete instructions saved this way will persist even after the app is closed, in
|
|
* which case they will be sent the next time the app is opened. If more than 10MB of commands are
|
|
* waiting to be sent, subsequent calls to {@code #deleteEventually()} or
|
|
* {@link #saveEventually()} will cause old instructions to be silently discarded until the
|
|
* connection can be re-established, and the queued objects can be saved.
|
|
*
|
|
* @param callback
|
|
* - A callback which will be called if the delete completes before the app exits.
|
|
*/
|
|
public final void deleteEventually(DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(deleteEventually(), callback);
|
|
}
|
|
|
|
/**
|
|
* Deletes this object from the server at some unspecified time in the future, even if Parse is
|
|
* currently inaccessible. Use this when you may not have a solid network connection, and don't
|
|
* need to know when the delete completes. If there is some problem with the object such that it
|
|
* can't be deleted, the request will be silently discarded. Delete requests made with this method
|
|
* will be stored locally in an on-disk cache until they can be transmitted to Parse. They will be
|
|
* sent immediately if possible. Otherwise, they will be sent the next time a network connection
|
|
* is available. Delete instructions saved this way will persist even after the app is closed, in
|
|
* which case they will be sent the next time the app is opened. If more than 10MB of commands are
|
|
* waiting to be sent, subsequent calls to {@code #deleteEventually()} or
|
|
* {@link #saveEventually()} will cause old instructions to be silently discarded until the
|
|
* connection can be re-established, and the queued objects can be saved.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when the delete completes.
|
|
*/
|
|
public final Task<Void> deleteEventually() {
|
|
final ParseRESTCommand command;
|
|
final Task<JSONObject> runEventuallyTask;
|
|
synchronized (mutex) {
|
|
validateDelete();
|
|
isDeletingEventually += 1;
|
|
|
|
String localId = null;
|
|
if (getObjectId() == null) {
|
|
localId = getOrCreateLocalId();
|
|
}
|
|
|
|
// TODO(grantland): Convert to async
|
|
final String sessionToken = ParseUser.getCurrentSessionToken();
|
|
|
|
// See [1]
|
|
command = ParseRESTObjectCommand.deleteObjectCommand(
|
|
getState(), sessionToken);
|
|
command.setLocalId(localId);
|
|
|
|
runEventuallyTask = Parse.getEventuallyQueue().enqueueEventuallyAsync(command, ParseObject.this);
|
|
}
|
|
|
|
Task<Void> handleDeleteResultTask;
|
|
if (Parse.isLocalDatastoreEnabled()) {
|
|
// ParsePinningEventuallyQueue calls handleDeleteEventuallyResultAsync directly.
|
|
handleDeleteResultTask = runEventuallyTask.makeVoid();
|
|
} else {
|
|
handleDeleteResultTask = runEventuallyTask.onSuccessTask(new Continuation<JSONObject, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<JSONObject> task) throws Exception {
|
|
return handleDeleteEventuallyResultAsync();
|
|
}
|
|
});
|
|
}
|
|
|
|
return handleDeleteResultTask;
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code deleteEventually}.
|
|
*
|
|
* Should only be called on success.
|
|
*/
|
|
/* package */ Task<Void> handleDeleteEventuallyResultAsync() {
|
|
synchronized (mutex) {
|
|
isDeletingEventually -= 1;
|
|
}
|
|
Task<Void> handleDeleteResultTask = handleDeleteResultAsync();
|
|
|
|
return handleDeleteResultTask.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
Parse.getEventuallyQueue()
|
|
.notifyTestHelper(ParseCommandCache.TestHelper.OBJECT_REMOVED);
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code fetch}.
|
|
*
|
|
* Should only be called on success.
|
|
*/
|
|
/* package */ Task<Void> handleFetchResultAsync(final ParseObject.State result) {
|
|
Task<Void> task = Task.forResult(null);
|
|
|
|
/*
|
|
* If this object is in the offline store, then we need to make sure that we pull in any dirty
|
|
* changes it may have before merging the server data into it.
|
|
*/
|
|
final OfflineStore store = Parse.getLocalDatastore();
|
|
if (store != null) {
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return store.fetchLocallyAsync(ParseObject.this).makeVoid();
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// Catch CACHE_MISS
|
|
if (task.getError() instanceof ParseException
|
|
&& ((ParseException)task.getError()).getCode() == ParseException.CACHE_MISS) {
|
|
return null;
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
synchronized (mutex) {
|
|
State newState;
|
|
if (result.isComplete()) {
|
|
// Result is complete, so just replace
|
|
newState = result;
|
|
} else {
|
|
// Result is incomplete, so we'll need to apply it to the current state
|
|
newState = getState().newBuilder().apply(result).build();
|
|
}
|
|
setState(newState);
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (store != null) {
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return store.updateDataForObjectAsync(ParseObject.this);
|
|
}
|
|
}).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// Catch CACHE_MISS
|
|
if (task.getError() instanceof ParseException
|
|
&& ((ParseException) task.getError()).getCode() == ParseException.CACHE_MISS) {
|
|
return null;
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Refreshes this object with the data from the server. Call this whenever you want the state of
|
|
* the object to reflect exactly what is on the server.
|
|
*
|
|
* @throws ParseException
|
|
* Throws an exception if the server is inaccessible.
|
|
*
|
|
* @deprecated Please use {@link #fetch()} instead.
|
|
*/
|
|
@Deprecated
|
|
public final void refresh() throws ParseException {
|
|
fetch();
|
|
}
|
|
|
|
/**
|
|
* Refreshes this object with the data from the server in a background thread. This is preferable
|
|
* to using refresh(), unless your code is already running from a background thread.
|
|
*
|
|
* @param callback
|
|
* {@code callback.done(object, e)} is called when the refresh completes.
|
|
*
|
|
* @deprecated Please use {@link #fetchInBackground(GetCallback)} instead.
|
|
*/
|
|
@Deprecated
|
|
public final void refreshInBackground(RefreshCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(fetchInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* Fetches this object with the data from the server. Call this whenever you want the state of the
|
|
* object to reflect exactly what is on the server.
|
|
*
|
|
* @throws ParseException
|
|
* Throws an exception if the server is inaccessible.
|
|
* @return The {@code ParseObject} that was fetched.
|
|
*/
|
|
public <T extends ParseObject> T fetch() throws ParseException {
|
|
return ParseTaskUtils.wait(this.<T>fetchInBackground());
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
/* package */ <T extends ParseObject> Task<T> fetchAsync(
|
|
final String sessionToken, Task<Void> toAwait) {
|
|
return toAwait.onSuccessTask(new Continuation<Void, Task<ParseObject.State>>() {
|
|
@Override
|
|
public Task<ParseObject.State> then(Task<Void> task) throws Exception {
|
|
State state;
|
|
Map<String, ParseObject> fetchedObjects;
|
|
synchronized (mutex) {
|
|
state = getState();
|
|
fetchedObjects = collectFetchedObjects();
|
|
}
|
|
ParseDecoder decoder = new KnownParseObjectDecoder(fetchedObjects);
|
|
return getObjectController().fetchAsync(state, sessionToken, decoder);
|
|
}
|
|
}).onSuccessTask(new Continuation<ParseObject.State, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<ParseObject.State> task) throws Exception {
|
|
ParseObject.State result = task.getResult();
|
|
return handleFetchResultAsync(result);
|
|
}
|
|
}).onSuccess(new Continuation<Void, T>() {
|
|
@Override
|
|
public T then(Task<Void> task) throws Exception {
|
|
return (T) ParseObject.this;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches this object with the data from the server in a background thread. This is preferable to
|
|
* using fetch(), unless your code is already running from a background thread.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when fetch completes.
|
|
*/
|
|
public final <T extends ParseObject> Task<T> fetchInBackground() {
|
|
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<String> task) throws Exception {
|
|
final String sessionToken = task.getResult();
|
|
return taskQueue.enqueue(new Continuation<Void, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<Void> toAwait) throws Exception {
|
|
return fetchAsync(sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches this object with the data from the server in a background thread. This is preferable to
|
|
* using fetch(), unless your code is already running from a background thread.
|
|
*
|
|
* @param callback
|
|
* {@code callback.done(object, e)} is called when the fetch completes.
|
|
*/
|
|
public final <T extends ParseObject> void fetchInBackground(GetCallback<T> callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(this.<T>fetchInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}),
|
|
* fetches this object with the data from the server in a background thread. This is preferable to
|
|
* using {@link #fetchIfNeeded()}, unless your code is already running from a background thread.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when fetch completes.
|
|
*/
|
|
public final <T extends ParseObject> Task<T> fetchIfNeededInBackground() {
|
|
if (isDataAvailable()) {
|
|
return Task.forResult((T) this);
|
|
}
|
|
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<String> task) throws Exception {
|
|
final String sessionToken = task.getResult();
|
|
return taskQueue.enqueue(new Continuation<Void, Task<T>>() {
|
|
@Override
|
|
public Task<T> then(Task<Void> toAwait) throws Exception {
|
|
if (isDataAvailable()) {
|
|
return Task.forResult((T) ParseObject.this);
|
|
}
|
|
return fetchAsync(sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}),
|
|
* fetches this object with the data from the server.
|
|
*
|
|
* @throws ParseException
|
|
* Throws an exception if the server is inaccessible.
|
|
* @return The fetched {@code ParseObject}.
|
|
*/
|
|
public <T extends ParseObject> T fetchIfNeeded() throws ParseException {
|
|
return ParseTaskUtils.wait(this.<T>fetchIfNeededInBackground());
|
|
}
|
|
|
|
/**
|
|
* If this {@code ParseObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}),
|
|
* fetches this object with the data from the server in a background thread. This is preferable to
|
|
* using {@link #fetchIfNeeded()}, unless your code is already running from a background thread.
|
|
*
|
|
* @param callback
|
|
* {@code callback.done(object, e)} is called when the fetch completes.
|
|
*/
|
|
public final <T extends ParseObject> void fetchIfNeededInBackground(GetCallback<T> callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(this.<T>fetchIfNeededInBackground(), callback);
|
|
}
|
|
|
|
// Validates the delete method
|
|
/* package */ void validateDelete() {
|
|
// do nothing
|
|
}
|
|
|
|
private Task<Void> deleteAsync(final String sessionToken, Task<Void> toAwait) {
|
|
validateDelete();
|
|
|
|
return toAwait.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
isDeleting = true;
|
|
if (state.objectId() == null) {
|
|
return task.cast(); // no reason to call delete since it doesn't exist
|
|
}
|
|
return deleteAsync(sessionToken);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return handleDeleteResultAsync();
|
|
}
|
|
}).continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
isDeleting = false;
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
//TODO (grantland): I'm not sure we want direct access to this. All access to `delete` should
|
|
// enqueue on the taskQueue...
|
|
/* package */ Task<Void> deleteAsync(String sessionToken) throws ParseException {
|
|
return getObjectController().deleteAsync(getState(), sessionToken);
|
|
}
|
|
|
|
/**
|
|
* Handles the result of {@code delete}.
|
|
*
|
|
* Should only be called on success.
|
|
*/
|
|
/* package */ Task<Void> handleDeleteResultAsync() {
|
|
Task<Void> task = Task.forResult(null);
|
|
|
|
synchronized (mutex) {
|
|
isDeleted = true;
|
|
}
|
|
|
|
final OfflineStore store = Parse.getLocalDatastore();
|
|
if (store != null) {
|
|
task = task.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
synchronized (mutex) {
|
|
if (isDeleted) {
|
|
store.unregisterObject(ParseObject.this);
|
|
return store.deleteDataForObjectAsync(ParseObject.this);
|
|
} else {
|
|
return store.updateDataForObjectAsync(ParseObject.this);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
/**
|
|
* Deletes this object on the server in a background thread. This is preferable to using
|
|
* {@link #delete()}, unless your code is already running from a background thread.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when delete completes.
|
|
*/
|
|
public final Task<Void> deleteInBackground() {
|
|
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
final String sessionToken = task.getResult();
|
|
return taskQueue.enqueue(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
return deleteAsync(sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deletes this object on the server. This does not delete or destroy the object locally.
|
|
*
|
|
* @throws ParseException
|
|
* Throws an error if the object does not exist or if the internet fails.
|
|
*/
|
|
public final void delete() throws ParseException {
|
|
ParseTaskUtils.wait(deleteInBackground());
|
|
}
|
|
|
|
/**
|
|
* Deletes this object on the server in a background thread. This is preferable to using
|
|
* {@link #delete()}, unless your code is already running from a background thread.
|
|
*
|
|
* @param callback
|
|
* {@code callback.done(e)} is called when the save completes.
|
|
*/
|
|
public final void deleteInBackground(DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(deleteInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* This deletes all of the objects from the given List.
|
|
*/
|
|
private static <T extends ParseObject> Task<Void> deleteAllAsync(
|
|
final List<T> objects, final String sessionToken) {
|
|
if (objects.size() == 0) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
// Create a list of unique objects based on objectIds
|
|
int objectCount = objects.size();
|
|
final List<ParseObject> uniqueObjects = new ArrayList<>(objectCount);
|
|
final HashSet<String> idSet = new HashSet<>();
|
|
for (int i = 0; i < objectCount; i++) {
|
|
ParseObject obj = objects.get(i);
|
|
if (!idSet.contains(obj.getObjectId())) {
|
|
idSet.add(obj.getObjectId());
|
|
uniqueObjects.add(obj);
|
|
}
|
|
}
|
|
|
|
return enqueueForAll(uniqueObjects, new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
return deleteAllAsync(uniqueObjects, sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
|
|
private static <T extends ParseObject> Task<Void> deleteAllAsync(
|
|
final List<T> uniqueObjects, final String sessionToken, Task<Void> toAwait) {
|
|
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
int objectCount = uniqueObjects.size();
|
|
List<ParseObject.State> states = new ArrayList<>(objectCount);
|
|
for (int i = 0; i < objectCount; i++) {
|
|
ParseObject object = uniqueObjects.get(i);
|
|
object.validateDelete();
|
|
states.add(object.getState());
|
|
}
|
|
List<Task<Void>> batchTasks = getObjectController().deleteAllAsync(states, sessionToken);
|
|
|
|
List<Task<Void>> tasks = new ArrayList<>(objectCount);
|
|
for (int i = 0; i < objectCount; i++) {
|
|
Task<Void> batchTask = batchTasks.get(i);
|
|
final T object = uniqueObjects.get(i);
|
|
tasks.add(batchTask.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(final Task<Void> batchTask) throws Exception {
|
|
return object.handleDeleteResultAsync().continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return batchTask;
|
|
}
|
|
});
|
|
}
|
|
}));
|
|
}
|
|
return Task.whenAll(tasks);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Deletes each object in the provided list. This is faster than deleting each object individually
|
|
* because it batches the requests.
|
|
*
|
|
* @param objects
|
|
* The objects to delete.
|
|
* @throws ParseException
|
|
* Throws an exception if the server returns an error or is inaccessible.
|
|
*/
|
|
public static <T extends ParseObject> void deleteAll(List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(deleteAllInBackground(objects));
|
|
}
|
|
|
|
/**
|
|
* Deletes each object in the provided list. This is faster than deleting each object individually
|
|
* because it batches the requests.
|
|
*
|
|
* @param objects
|
|
* The objects to delete.
|
|
* @param callback
|
|
* The callback method to execute when completed.
|
|
*/
|
|
public static <T extends ParseObject> void deleteAllInBackground(List<T> objects, DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(deleteAllInBackground(objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Deletes each object in the provided list. This is faster than deleting each object individually
|
|
* because it batches the requests.
|
|
*
|
|
* @param objects
|
|
* The objects to delete.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when deleteAll completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> deleteAllInBackground(final List<T> objects) {
|
|
return ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
String sessionToken = task.getResult();
|
|
return deleteAllAsync(objects, sessionToken);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Finds all of the objects that are reachable from child, including child itself, and adds them
|
|
* to the given mutable array. It traverses arrays and json objects.
|
|
*
|
|
* @param node
|
|
* An kind object to search for children.
|
|
* @param dirtyChildren
|
|
* The array to collect the {@code ParseObject}s into.
|
|
* @param dirtyFiles
|
|
* The array to collect the {@link ParseFile}s into.
|
|
* @param alreadySeen
|
|
* The set of all objects that have already been seen.
|
|
* @param alreadySeenNew
|
|
* The set of new objects that have already been seen since the last existing object.
|
|
*/
|
|
private static void collectDirtyChildren(Object node,
|
|
final Collection<ParseObject> dirtyChildren,
|
|
final Collection<ParseFile> dirtyFiles,
|
|
final Set<ParseObject> alreadySeen,
|
|
final Set<ParseObject> alreadySeenNew) {
|
|
|
|
new ParseTraverser() {
|
|
@Override
|
|
protected boolean visit(Object node) {
|
|
// If it's a file, then add it to the list if it's dirty.
|
|
if (node instanceof ParseFile) {
|
|
if (dirtyFiles == null) {
|
|
return true;
|
|
}
|
|
|
|
ParseFile file = (ParseFile) node;
|
|
if (file.getUrl() == null) {
|
|
dirtyFiles.add(file);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// If it's anything other than a file, then just continue;
|
|
if (!(node instanceof ParseObject)) {
|
|
return true;
|
|
}
|
|
|
|
if (dirtyChildren == null) {
|
|
return true;
|
|
}
|
|
|
|
// For files, we need to handle recursion manually to find cycles of new objects.
|
|
ParseObject object = (ParseObject) node;
|
|
Set<ParseObject> seen = alreadySeen;
|
|
Set<ParseObject> seenNew = alreadySeenNew;
|
|
|
|
// Check for cycles of new objects. Any such cycle means it will be
|
|
// impossible to save this collection of objects, so throw an exception.
|
|
if (object.getObjectId() != null) {
|
|
seenNew = new HashSet<>();
|
|
} else {
|
|
if (seenNew.contains(object)) {
|
|
throw new RuntimeException("Found a circular dependency while saving.");
|
|
}
|
|
seenNew = new HashSet<>(seenNew);
|
|
seenNew.add(object);
|
|
}
|
|
|
|
// Check for cycles of any object. If this occurs, then there's no
|
|
// problem, but we shouldn't recurse any deeper, because it would be
|
|
// an infinite recursion.
|
|
if (seen.contains(object)) {
|
|
return true;
|
|
}
|
|
seen = new HashSet<>(seen);
|
|
seen.add(object);
|
|
|
|
// Recurse into this object's children looking for dirty children.
|
|
// We only need to look at the child object's current estimated data,
|
|
// because that's the only data that might need to be saved now.
|
|
collectDirtyChildren(object.estimatedData, dirtyChildren, dirtyFiles, seen, seenNew);
|
|
|
|
if (object.isDirty(false)) {
|
|
dirtyChildren.add(object);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}.setYieldRoot(true).traverse(node);
|
|
}
|
|
|
|
/**
|
|
* Helper version of collectDirtyChildren so that callers don't have to add the internally used
|
|
* parameters.
|
|
*/
|
|
private static void collectDirtyChildren(Object node, Collection<ParseObject> dirtyChildren,
|
|
Collection<ParseFile> dirtyFiles) {
|
|
collectDirtyChildren(node, dirtyChildren, dirtyFiles,
|
|
new HashSet<ParseObject>(),
|
|
new HashSet<ParseObject>());
|
|
}
|
|
|
|
/**
|
|
* Returns {@code true} if this object can be serialized for saving.
|
|
*/
|
|
private boolean canBeSerialized() {
|
|
synchronized (mutex) {
|
|
final Capture<Boolean> result = new Capture<>(true);
|
|
|
|
// This method is only used for batching sets of objects for saveAll
|
|
// and when saving children automatically. Since it's only used to
|
|
// determine whether or not save should be called on them, it only
|
|
// needs to examine their current values, so we use estimatedData.
|
|
new ParseTraverser() {
|
|
@Override
|
|
protected boolean visit(Object value) {
|
|
if (value instanceof ParseFile) {
|
|
ParseFile file = (ParseFile) value;
|
|
if (file.isDirty()) {
|
|
result.set(false);
|
|
}
|
|
}
|
|
|
|
if (value instanceof ParseObject) {
|
|
ParseObject object = (ParseObject) value;
|
|
if (object.getObjectId() == null) {
|
|
result.set(false);
|
|
}
|
|
}
|
|
|
|
// Continue to traverse only if it can still be serialized.
|
|
return result.get();
|
|
}
|
|
}.setYieldRoot(false).setTraverseParseObjects(true).traverse(this);
|
|
|
|
return result.get();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This saves all of the objects and files reachable from the given object. It does its work in
|
|
* multiple waves, saving as many as possible in each wave. If there's ever an error, it just
|
|
* gives up, sets error, and returns NO.
|
|
*/
|
|
private static Task<Void> deepSaveAsync(final Object object, final String sessionToken) {
|
|
Set<ParseObject> objects = new HashSet<>();
|
|
Set<ParseFile> files = new HashSet<>();
|
|
collectDirtyChildren(object, objects, files);
|
|
|
|
// This has to happen separately from everything else because ParseUser.save() is
|
|
// special-cased to work for lazy users, but new users can't be created by
|
|
// ParseMultiCommand's regular save.
|
|
Set<ParseUser> users = new HashSet<>();
|
|
for (ParseObject o : objects) {
|
|
if (o instanceof ParseUser) {
|
|
ParseUser user = (ParseUser) o;
|
|
if (user.isLazy()) {
|
|
users.add((ParseUser) o);
|
|
}
|
|
}
|
|
}
|
|
objects.removeAll(users);
|
|
|
|
// objects will need to wait for files to be complete since they may be nested children.
|
|
final AtomicBoolean filesComplete = new AtomicBoolean(false);
|
|
List<Task<Void>> tasks = new ArrayList<>();
|
|
for (ParseFile file : files) {
|
|
tasks.add(file.saveAsync(sessionToken, null, null));
|
|
}
|
|
Task<Void> filesTask = Task.whenAll(tasks).continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
filesComplete.set(true);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// objects will need to wait for users to be complete since they may be nested children.
|
|
final AtomicBoolean usersComplete = new AtomicBoolean(false);
|
|
tasks = new ArrayList<>();
|
|
for (final ParseUser user : users) {
|
|
tasks.add(user.saveAsync(sessionToken));
|
|
}
|
|
Task<Void> usersTask = Task.whenAll(tasks).continueWith(new Continuation<Void, Void>() {
|
|
@Override
|
|
public Void then(Task<Void> task) throws Exception {
|
|
usersComplete.set(true);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
final Capture<Set<ParseObject>> remaining = new Capture<>(objects);
|
|
Task<Void> objectsTask = Task.forResult(null).continueWhile(new Callable<Boolean>() {
|
|
@Override
|
|
public Boolean call() throws Exception {
|
|
return remaining.get().size() > 0;
|
|
}
|
|
}, new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// Partition the objects into two sets: those that can be save immediately,
|
|
// and those that rely on other objects to be created first.
|
|
final List<ParseObject> current = new ArrayList<>();
|
|
final Set<ParseObject> nextBatch = new HashSet<>();
|
|
for (ParseObject obj : remaining.get()) {
|
|
if (obj.canBeSerialized()) {
|
|
current.add(obj);
|
|
} else {
|
|
nextBatch.add(obj);
|
|
}
|
|
}
|
|
remaining.set(nextBatch);
|
|
|
|
if (current.size() == 0 && filesComplete.get() && usersComplete.get()) {
|
|
// We do cycle-detection when building the list of objects passed to this function, so
|
|
// this should never get called. But we should check for it anyway, so that we get an
|
|
// exception instead of an infinite loop.
|
|
throw new RuntimeException("Unable to save a ParseObject with a relation to a cycle.");
|
|
}
|
|
|
|
// Package all save commands together
|
|
if (current.size() == 0) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
return enqueueForAll(current, new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> toAwait) throws Exception {
|
|
return saveAllAsync(current, sessionToken, toAwait);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return Task.whenAll(Arrays.asList(filesTask, usersTask, objectsTask));
|
|
}
|
|
|
|
private static <T extends ParseObject> Task<Void> saveAllAsync(
|
|
final List<T> uniqueObjects, final String sessionToken, Task<Void> toAwait) {
|
|
return toAwait.continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
int objectCount = uniqueObjects.size();
|
|
List<ParseObject.State> states = new ArrayList<>(objectCount);
|
|
List<ParseOperationSet> operationsList = new ArrayList<>(objectCount);
|
|
List<ParseDecoder> decoders = new ArrayList<>(objectCount);
|
|
for (int i = 0; i < objectCount; i++) {
|
|
ParseObject object = uniqueObjects.get(i);
|
|
object.updateBeforeSave();
|
|
object.validateSave();
|
|
|
|
states.add(object.getState());
|
|
operationsList.add(object.startSave());
|
|
final Map<String, ParseObject> fetchedObjects = object.collectFetchedObjects();
|
|
decoders.add(new KnownParseObjectDecoder(fetchedObjects));
|
|
}
|
|
List<Task<ParseObject.State>> batchTasks = getObjectController().saveAllAsync(
|
|
states, operationsList, sessionToken, decoders);
|
|
|
|
List<Task<Void>> tasks = new ArrayList<>(objectCount);
|
|
for (int i = 0; i < objectCount; i++) {
|
|
Task<ParseObject.State> batchTask = batchTasks.get(i);
|
|
final T object = uniqueObjects.get(i);
|
|
final ParseOperationSet operations = operationsList.get(i);
|
|
tasks.add(batchTask.continueWithTask(new Continuation<ParseObject.State, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(final Task<ParseObject.State> batchTask) throws Exception {
|
|
ParseObject.State result = batchTask.getResult(); // will be null on failure
|
|
return object.handleSaveResultAsync(result, operations).continueWithTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
if (task.isFaulted() || task.isCancelled()) {
|
|
return task;
|
|
}
|
|
|
|
// We still want to propagate batchTask errors
|
|
return batchTask.makeVoid();
|
|
}
|
|
});
|
|
}
|
|
}));
|
|
}
|
|
return Task.whenAll(tasks);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Saves each object in the provided list. This is faster than saving each object individually
|
|
* because it batches the requests.
|
|
*
|
|
* @param objects
|
|
* The objects to save.
|
|
* @throws ParseException
|
|
* Throws an exception if the server returns an error or is inaccessible.
|
|
*/
|
|
public static <T extends ParseObject> void saveAll(List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(saveAllInBackground(objects));
|
|
}
|
|
|
|
/**
|
|
* Saves each object in the provided list to the server in a background thread. This is preferable
|
|
* to using saveAll, unless your code is already running from a background thread.
|
|
*
|
|
* @param objects
|
|
* The objects to save.
|
|
* @param callback
|
|
* {@code callback.done(e)} is called when the save completes.
|
|
*/
|
|
public static <T extends ParseObject> void saveAllInBackground(List<T> objects, SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(saveAllInBackground(objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Saves each object in the provided list to the server in a background thread. This is preferable
|
|
* to using saveAll, unless your code is already running from a background thread.
|
|
*
|
|
* @param objects
|
|
* The objects to save.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when saveAll completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> saveAllInBackground(final List<T> objects) {
|
|
return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation<ParseUser, Task<String>>() {
|
|
@Override
|
|
public Task<String> then(Task<ParseUser> task) throws Exception {
|
|
final ParseUser current = task.getResult();
|
|
if (current == null) {
|
|
return Task.forResult(null);
|
|
}
|
|
if (!current.isLazy()) {
|
|
return Task.forResult(current.getSessionToken());
|
|
}
|
|
|
|
// The current user is lazy/unresolved. If it is attached to any of the objects via ACL,
|
|
// we'll need to resolve/save it before proceeding.
|
|
for (ParseObject object : objects) {
|
|
if (!object.isDataAvailable(KEY_ACL)) {
|
|
continue;
|
|
}
|
|
final ParseACL acl = object.getACL(false);
|
|
if (acl == null) {
|
|
continue;
|
|
}
|
|
final ParseUser user = acl.getUnresolvedUser();
|
|
if (user != null && user.isCurrentUser()) {
|
|
// We only need to find one, since there's only one current user.
|
|
return user.saveAsync(null).onSuccess(new Continuation<Void, String>() {
|
|
@Override
|
|
public String then(Task<Void> task) throws Exception {
|
|
if (acl.hasUnresolvedUser()) {
|
|
throw new IllegalStateException("ACL has an unresolved ParseUser. "
|
|
+ "Save or sign up before attempting to serialize the ACL.");
|
|
}
|
|
return user.getSessionToken();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// There were no objects with ACLs pointing to unresolved users.
|
|
return Task.forResult(null);
|
|
}
|
|
}).onSuccessTask(new Continuation<String, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<String> task) throws Exception {
|
|
final String sessionToken = task.getResult();
|
|
return deepSaveAsync(objects, sessionToken);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects that don't have data in the provided list in the background.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when fetchAllIfNeeded completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<List<T>> fetchAllIfNeededInBackground(
|
|
final List<T> objects) {
|
|
return fetchAllAsync(objects, true);
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects that don't have data in the provided list.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
* @return The list passed in for convenience.
|
|
* @throws ParseException
|
|
* Throws an exception if the server returns an error or is inaccessible.
|
|
*/
|
|
public static <T extends ParseObject> List<T> fetchAllIfNeeded(List<T> objects)
|
|
throws ParseException {
|
|
return ParseTaskUtils.wait(fetchAllIfNeededInBackground(objects));
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects that don't have data in the provided list in the background.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
* @param callback
|
|
* {@code callback.done(result, e)} is called when the fetch completes.
|
|
*/
|
|
public static <T extends ParseObject> void fetchAllIfNeededInBackground(final List<T> objects,
|
|
FindCallback<T> callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(fetchAllIfNeededInBackground(objects), callback);
|
|
}
|
|
|
|
private static <T extends ParseObject> Task<List<T>> fetchAllAsync(
|
|
final List<T> objects, final boolean onlyIfNeeded) {
|
|
return ParseUser.getCurrentUserAsync().onSuccessTask(new Continuation<ParseUser, Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> then(Task<ParseUser> task) throws Exception {
|
|
final ParseUser user = task.getResult();
|
|
return enqueueForAll(objects, new Continuation<Void, Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> then(Task<Void> task) throws Exception {
|
|
return fetchAllAsync(objects, user, onlyIfNeeded, task);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param onlyIfNeeded If enabled, will only fetch if the object has an objectId and
|
|
* !isDataAvailable, otherwise it requires objectIds and will fetch regardless
|
|
* of data availability.
|
|
*/
|
|
// TODO(grantland): Convert to ParseUser.State
|
|
private static <T extends ParseObject> Task<List<T>> fetchAllAsync(
|
|
final List<T> objects, final ParseUser user, final boolean onlyIfNeeded, Task<Void> toAwait) {
|
|
if (objects.size() == 0) {
|
|
return Task.forResult(objects);
|
|
}
|
|
|
|
List<String> objectIds = new ArrayList<>();
|
|
String className = null;
|
|
for (T object : objects) {
|
|
if (onlyIfNeeded && object.isDataAvailable()) {
|
|
continue;
|
|
}
|
|
|
|
if (className != null && !object.getClassName().equals(className)) {
|
|
throw new IllegalArgumentException("All objects should have the same class");
|
|
}
|
|
className = object.getClassName();
|
|
|
|
String objectId = object.getObjectId();
|
|
if (objectId != null) {
|
|
objectIds.add(object.getObjectId());
|
|
} else if (!onlyIfNeeded) {
|
|
throw new IllegalArgumentException("All objects must exist on the server");
|
|
}
|
|
}
|
|
|
|
if (objectIds.size() == 0) {
|
|
return Task.forResult(objects);
|
|
}
|
|
|
|
final ParseQuery<T> query = ParseQuery.<T>getQuery(className)
|
|
.whereContainedIn(KEY_OBJECT_ID, objectIds);
|
|
return toAwait.continueWithTask(new Continuation<Void, Task<List<T>>>() {
|
|
@Override
|
|
public Task<List<T>> then(Task<Void> task) throws Exception {
|
|
return query.findAsync(query.getBuilder().build(), user, null);
|
|
}
|
|
}).onSuccess(new Continuation<List<T>, List<T>>() {
|
|
@Override
|
|
public List<T> then(Task<List<T>> task) throws Exception {
|
|
Map<String, T> resultMap = new HashMap<>();
|
|
for (T o : task.getResult()) {
|
|
resultMap.put(o.getObjectId(), o);
|
|
}
|
|
for (T object : objects) {
|
|
if (onlyIfNeeded && object.isDataAvailable()) {
|
|
continue;
|
|
}
|
|
|
|
T newObject = resultMap.get(object.getObjectId());
|
|
if (newObject == null) {
|
|
throw new ParseException(
|
|
ParseException.OBJECT_NOT_FOUND,
|
|
"Object id " + object.getObjectId() + " does not exist");
|
|
}
|
|
if (!Parse.isLocalDatastoreEnabled()) {
|
|
// We only need to merge if LDS is disabled, since single instance will do the merging
|
|
// for us.
|
|
object.mergeFromObject(newObject);
|
|
}
|
|
}
|
|
return objects;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects in the provided list in the background.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when fetch completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<List<T>> fetchAllInBackground(final List<T> objects) {
|
|
return fetchAllAsync(objects, false);
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects in the provided list.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
* @return The list passed in.
|
|
* @throws ParseException
|
|
* Throws an exception if the server returns an error or is inaccessible.
|
|
*/
|
|
public static <T extends ParseObject> List<T> fetchAll(List<T> objects) throws ParseException {
|
|
return ParseTaskUtils.wait(fetchAllInBackground(objects));
|
|
}
|
|
|
|
/**
|
|
* Fetches all the objects in the provided list in the background.
|
|
*
|
|
* @param objects
|
|
* The list of objects to fetch.
|
|
* @param callback
|
|
* {@code callback.done(result, e)} is called when the fetch completes.
|
|
*/
|
|
public static <T extends ParseObject> void fetchAllInBackground(List<T> objects,
|
|
FindCallback<T> callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(fetchAllInBackground(objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Return the operations that will be sent in the next call to save.
|
|
*/
|
|
private ParseOperationSet currentOperations() {
|
|
synchronized (mutex) {
|
|
return operationSetQueue.getLast();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the estimated values in the map based on the given set of ParseFieldOperations.
|
|
*/
|
|
private void applyOperations(ParseOperationSet operations, Map<String, Object> map) {
|
|
for (String key : operations.keySet()) {
|
|
ParseFieldOperation operation = operations.get(key);
|
|
Object oldValue = map.get(key);
|
|
Object newValue = operation.apply(oldValue, key);
|
|
if (newValue != null) {
|
|
map.put(key, newValue);
|
|
} else {
|
|
map.remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regenerates the estimatedData map from the serverData and operations.
|
|
*/
|
|
private void rebuildEstimatedData() {
|
|
synchronized (mutex) {
|
|
estimatedData.clear();
|
|
for (String key : state.keySet()) {
|
|
estimatedData.put(key, state.get(key));
|
|
}
|
|
for (ParseOperationSet operations : operationSetQueue) {
|
|
applyOperations(operations, estimatedData);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* package */ void markAllFieldsDirty() {
|
|
synchronized (mutex) {
|
|
for (String key : state.keySet()) {
|
|
performPut(key, state.get(key));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* performOperation() is like {@link #put(String, Object)} but instead of just taking a new value,
|
|
* it takes a ParseFieldOperation that modifies the value.
|
|
*/
|
|
/* package */ void performOperation(String key, ParseFieldOperation operation) {
|
|
synchronized (mutex) {
|
|
Object oldValue = estimatedData.get(key);
|
|
Object newValue = operation.apply(oldValue, key);
|
|
if (newValue != null) {
|
|
estimatedData.put(key, newValue);
|
|
} else {
|
|
estimatedData.remove(key);
|
|
}
|
|
|
|
ParseFieldOperation oldOperation = currentOperations().get(key);
|
|
ParseFieldOperation newOperation = operation.mergeWithPrevious(oldOperation);
|
|
currentOperations().put(key, newOperation);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add a key-value pair to this object. It is recommended to name keys in
|
|
* <code>camelCaseLikeThis</code>.
|
|
*
|
|
* @param key
|
|
* Keys must be alphanumerical plus underscore, and start with a letter.
|
|
* @param value
|
|
* Values may be numerical, {@link String}, {@link JSONObject}, {@link JSONArray},
|
|
* {@link JSONObject#NULL}, or other {@code ParseObject}s. value may not be {@code null}.
|
|
*/
|
|
public void put(String key, Object value) {
|
|
checkKeyIsMutable(key);
|
|
|
|
performPut(key, value);
|
|
}
|
|
|
|
/* package */ void performPut(String key, Object value) {
|
|
if (key == null) {
|
|
throw new IllegalArgumentException("key may not be null.");
|
|
}
|
|
|
|
if (value == null) {
|
|
throw new IllegalArgumentException("value may not be null.");
|
|
}
|
|
|
|
if (value instanceof JSONObject) {
|
|
ParseDecoder decoder = ParseDecoder.get();
|
|
value = decoder.convertJSONObjectToMap((JSONObject) value);
|
|
} else if (value instanceof JSONArray) {
|
|
ParseDecoder decoder = ParseDecoder.get();
|
|
value = decoder.convertJSONArrayToList((JSONArray) value);
|
|
}
|
|
|
|
if (!ParseEncoder.isValidType(value)) {
|
|
throw new IllegalArgumentException("invalid type for value: " + value.getClass().toString());
|
|
}
|
|
|
|
performOperation(key, new ParseSetOperation(value));
|
|
}
|
|
|
|
/**
|
|
* Whether this object has a particular key. Same as {@link #containsKey(String)}.
|
|
*
|
|
* @param key
|
|
* The key to check for
|
|
* @return Whether this object contains the key
|
|
*/
|
|
public boolean has(String key) {
|
|
return containsKey(key);
|
|
}
|
|
|
|
/**
|
|
* Atomically increments the given key by 1.
|
|
*
|
|
* @param key
|
|
* The key to increment.
|
|
*/
|
|
public void increment(String key) {
|
|
increment(key, 1);
|
|
}
|
|
|
|
/**
|
|
* Atomically increments the given key by the given number.
|
|
*
|
|
* @param key
|
|
* The key to increment.
|
|
* @param amount
|
|
* The amount to increment by.
|
|
*/
|
|
public void increment(String key, Number amount) {
|
|
ParseIncrementOperation operation = new ParseIncrementOperation(amount);
|
|
performOperation(key, operation);
|
|
}
|
|
|
|
/**
|
|
* Atomically adds an object to the end of the array associated with a given key.
|
|
*
|
|
* @param key
|
|
* The key.
|
|
* @param value
|
|
* The object to add.
|
|
*/
|
|
public void add(String key, Object value) {
|
|
this.addAll(key, Arrays.asList(value));
|
|
}
|
|
|
|
/**
|
|
* Atomically adds the objects contained in a {@code Collection} to the end of the array
|
|
* associated with a given key.
|
|
*
|
|
* @param key
|
|
* The key.
|
|
* @param values
|
|
* The objects to add.
|
|
*/
|
|
public void addAll(String key, Collection<?> values) {
|
|
ParseAddOperation operation = new ParseAddOperation(values);
|
|
performOperation(key, operation);
|
|
}
|
|
|
|
/**
|
|
* Atomically adds an object to the array associated with a given key, only if it is not already
|
|
* present in the array. The position of the insert is not guaranteed.
|
|
*
|
|
* @param key
|
|
* The key.
|
|
* @param value
|
|
* The object to add.
|
|
*/
|
|
public void addUnique(String key, Object value) {
|
|
this.addAllUnique(key, Arrays.asList(value));
|
|
}
|
|
|
|
/**
|
|
* Atomically adds the objects contained in a {@code Collection} to the array associated with a
|
|
* given key, only adding elements which are not already present in the array. The position of the
|
|
* insert is not guaranteed.
|
|
*
|
|
* @param key
|
|
* The key.
|
|
* @param values
|
|
* The objects to add.
|
|
*/
|
|
public void addAllUnique(String key, Collection<?> values) {
|
|
ParseAddUniqueOperation operation = new ParseAddUniqueOperation(values);
|
|
performOperation(key, operation);
|
|
}
|
|
|
|
/**
|
|
* Removes a key from this object's data if it exists.
|
|
*
|
|
* @param key
|
|
* The key to remove.
|
|
*/
|
|
public void remove(String key) {
|
|
checkKeyIsMutable(key);
|
|
|
|
performRemove(key);
|
|
}
|
|
|
|
/* package */ void performRemove(String key) {
|
|
synchronized (mutex) {
|
|
Object object = get(key);
|
|
|
|
if (object != null) {
|
|
performOperation(key, ParseDeleteOperation.getInstance());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atomically removes all instances of the objects contained in a {@code Collection} from the
|
|
* array associated with a given key. To maintain consistency with the Java Collection API, there
|
|
* is no method removing all instances of a single object. Instead, you can call
|
|
* {@code parseObject.removeAll(key, Arrays.asList(value))}.
|
|
*
|
|
* @param key
|
|
* The key.
|
|
* @param values
|
|
* The objects to remove.
|
|
*/
|
|
public void removeAll(String key, Collection<?> values) {
|
|
checkKeyIsMutable(key);
|
|
|
|
ParseRemoveOperation operation = new ParseRemoveOperation(values);
|
|
performOperation(key, operation);
|
|
}
|
|
|
|
private void checkKeyIsMutable(String key) {
|
|
if (!isKeyMutable(key)) {
|
|
throw new IllegalArgumentException("Cannot modify `" + key
|
|
+ "` property of an " + getClassName() + " object.");
|
|
}
|
|
}
|
|
|
|
/* package */ boolean isKeyMutable(String key) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether this object has a particular key. Same as {@link #has(String)}.
|
|
*
|
|
* @param key
|
|
* The key to check for
|
|
* @return Whether this object contains the key
|
|
*/
|
|
public boolean containsKey(String key) {
|
|
synchronized (mutex) {
|
|
return estimatedData.containsKey(key);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Access a {@link String} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link String}.
|
|
*/
|
|
public String getString(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof String)) {
|
|
return null;
|
|
}
|
|
return (String) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@code byte[]} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@code byte[]}.
|
|
*/
|
|
public byte[] getBytes(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof byte[])) {
|
|
return null;
|
|
}
|
|
|
|
return (byte[]) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link Number} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link Number}.
|
|
*/
|
|
public Number getNumber(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof Number)) {
|
|
return null;
|
|
}
|
|
return (Number) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link JSONArray} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link JSONArray}.
|
|
*/
|
|
public JSONArray getJSONArray(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
|
|
if (value instanceof List) {
|
|
value = PointerOrLocalIdEncoder.get().encode(value);
|
|
}
|
|
|
|
if (!(value instanceof JSONArray)) {
|
|
return null;
|
|
}
|
|
return (JSONArray) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link List} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for
|
|
* @return {@code null} if there is no such key or if the value can't be converted to a
|
|
* {@link List}.
|
|
*/
|
|
public <T> List<T> getList(String key) {
|
|
synchronized (mutex) {
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof List)) {
|
|
return null;
|
|
}
|
|
@SuppressWarnings("unchecked")
|
|
List<T> returnValue = (List<T>) value;
|
|
return returnValue;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link Map} value
|
|
*
|
|
* @param key
|
|
* The key to access the value for
|
|
* @return {@code null} if there is no such key or if the value can't be converted to a
|
|
* {@link Map}.
|
|
*/
|
|
public <V> Map<String, V> getMap(String key) {
|
|
synchronized (mutex) {
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof Map)) {
|
|
return null;
|
|
}
|
|
@SuppressWarnings("unchecked")
|
|
Map<String, V> returnValue = (Map<String, V>) value;
|
|
return returnValue;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link JSONObject} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link JSONObject}.
|
|
*/
|
|
public JSONObject getJSONObject(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
|
|
if (value instanceof Map) {
|
|
value = PointerOrLocalIdEncoder.get().encode(value);
|
|
}
|
|
|
|
if (!(value instanceof JSONObject)) {
|
|
return null;
|
|
}
|
|
|
|
return (JSONObject) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access an {@code int} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code 0} if there is no such key or if it is not a {@code int}.
|
|
*/
|
|
public int getInt(String key) {
|
|
Number number = getNumber(key);
|
|
if (number == null) {
|
|
return 0;
|
|
}
|
|
return number.intValue();
|
|
}
|
|
|
|
/**
|
|
* Access a {@code double} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code 0} if there is no such key or if it is not a {@code double}.
|
|
*/
|
|
public double getDouble(String key) {
|
|
Number number = getNumber(key);
|
|
if (number == null) {
|
|
return 0;
|
|
}
|
|
return number.doubleValue();
|
|
}
|
|
|
|
/**
|
|
* Access a {@code long} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code 0} if there is no such key or if it is not a {@code long}.
|
|
*/
|
|
public long getLong(String key) {
|
|
Number number = getNumber(key);
|
|
if (number == null) {
|
|
return 0;
|
|
}
|
|
return number.longValue();
|
|
}
|
|
|
|
/**
|
|
* Access a {@code boolean} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code false} if there is no such key or if it is not a {@code boolean}.
|
|
*/
|
|
public boolean getBoolean(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof Boolean)) {
|
|
return false;
|
|
}
|
|
return (Boolean) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link Date} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link Date}.
|
|
*/
|
|
public Date getDate(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof Date)) {
|
|
return null;
|
|
}
|
|
return (Date) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@code ParseObject} value. This function will not perform a network request. Unless the
|
|
* {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling
|
|
* {@link #fetchIfNeeded()} or {@link #refresh()}), {@link #isDataAvailable()} will return
|
|
* {@code false}.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@code ParseObject}.
|
|
*/
|
|
public ParseObject getParseObject(String key) {
|
|
Object value = get(key);
|
|
if (!(value instanceof ParseObject)) {
|
|
return null;
|
|
}
|
|
return (ParseObject) value;
|
|
}
|
|
|
|
/**
|
|
* Access a {@link ParseUser} value. This function will not perform a network request. Unless the
|
|
* {@code ParseObject} has been downloaded (e.g. by a {@link ParseQuery#include(String)} or by calling
|
|
* {@link #fetchIfNeeded()} or {@link #refresh()}), {@link #isDataAvailable()} will return
|
|
* {@code false}.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if the value is not a {@link ParseUser}.
|
|
*/
|
|
public ParseUser getParseUser(String key) {
|
|
Object value = get(key);
|
|
if (!(value instanceof ParseUser)) {
|
|
return null;
|
|
}
|
|
return (ParseUser) value;
|
|
}
|
|
|
|
/**
|
|
* Access a {@link ParseFile} value. This function will not perform a network request. Unless the
|
|
* {@link ParseFile} has been downloaded (e.g. by calling {@link ParseFile#getData()}),
|
|
* {@link ParseFile#isDataAvailable()} will return {@code false}.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key or if it is not a {@link ParseFile}.
|
|
*/
|
|
public ParseFile getParseFile(String key) {
|
|
Object value = get(key);
|
|
if (!(value instanceof ParseFile)) {
|
|
return null;
|
|
}
|
|
return (ParseFile) value;
|
|
}
|
|
|
|
/**
|
|
* Access a {@link ParseGeoPoint} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for
|
|
* @return {@code null} if there is no such key or if it is not a {@link ParseGeoPoint}.
|
|
*/
|
|
public ParseGeoPoint getParseGeoPoint(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof ParseGeoPoint)) {
|
|
return null;
|
|
}
|
|
return (ParseGeoPoint) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a {@link ParsePolygon} value.
|
|
*
|
|
* @param key
|
|
* The key to access the value for
|
|
* @return {@code null} if there is no such key or if it is not a {@link ParsePolygon}.
|
|
*/
|
|
public ParsePolygon getParsePolygon(String key) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
if (!(value instanceof ParsePolygon)) {
|
|
return null;
|
|
}
|
|
return (ParsePolygon) value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access the {@link ParseACL} governing this object.
|
|
*/
|
|
public ParseACL getACL() {
|
|
return getACL(true);
|
|
}
|
|
|
|
private ParseACL getACL(boolean mayCopy) {
|
|
synchronized (mutex) {
|
|
checkGetAccess(KEY_ACL);
|
|
Object acl = estimatedData.get(KEY_ACL);
|
|
if (acl == null) {
|
|
return null;
|
|
}
|
|
if (!(acl instanceof ParseACL)) {
|
|
throw new RuntimeException("only ACLs can be stored in the ACL key");
|
|
}
|
|
if (mayCopy && ((ParseACL) acl).isShared()) {
|
|
ParseACL copy = new ParseACL((ParseACL) acl);
|
|
estimatedData.put(KEY_ACL, copy);
|
|
return copy;
|
|
}
|
|
return (ParseACL) acl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the {@link ParseACL} governing this object.
|
|
*/
|
|
public void setACL(ParseACL acl) {
|
|
put(KEY_ACL, acl);
|
|
}
|
|
|
|
/**
|
|
* Gets whether the {@code ParseObject} has been fetched.
|
|
*
|
|
* @return {@code true} if the {@code ParseObject} is new or has been fetched or refreshed. {@code false}
|
|
* otherwise.
|
|
*/
|
|
public boolean isDataAvailable() {
|
|
synchronized (mutex) {
|
|
return state.isComplete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// Fallback to estimatedData to include dirty changes.
|
|
return isDataAvailable() || state.availableKeys().contains(key) || estimatedData.containsKey(key);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access or create a {@link ParseRelation} value for a key
|
|
*
|
|
* @param key
|
|
* The key to access the relation for.
|
|
* @return the ParseRelation object if the relation already exists for the key or can be created
|
|
* for this key.
|
|
*/
|
|
public <T extends ParseObject> ParseRelation<T> getRelation(String key) {
|
|
synchronized (mutex) {
|
|
// All the sanity checking is done when add or remove is called on the relation.
|
|
Object value = estimatedData.get(key);
|
|
if (value instanceof ParseRelation) {
|
|
@SuppressWarnings("unchecked")
|
|
ParseRelation<T> relation = (ParseRelation<T>) value;
|
|
relation.ensureParentAndKey(this, key);
|
|
return relation;
|
|
} else {
|
|
ParseRelation<T> relation = new ParseRelation<>(this, key);
|
|
/*
|
|
* We put the relation into the estimated data so that we'll get the same instance later,
|
|
* which may have known objects cached. If we rebuildEstimatedData, then this relation will
|
|
* be lost, and we'll get a new one. That's okay, because any cached objects it knows about
|
|
* must be replayable from the operations in the queue. If there were any objects in this
|
|
* relation that weren't still in the queue, then they would be in the copy of the
|
|
* ParseRelation that's in the serverData, so we would have gotten that instance instead.
|
|
*/
|
|
estimatedData.put(key, relation);
|
|
return relation;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Access a value. In most cases it is more convenient to use a helper function such as
|
|
* {@link #getString(String)} or {@link #getInt(String)}.
|
|
*
|
|
* @param key
|
|
* The key to access the value for.
|
|
* @return {@code null} if there is no such key.
|
|
*/
|
|
public Object get(String key) {
|
|
synchronized (mutex) {
|
|
if (key.equals(KEY_ACL)) {
|
|
return getACL();
|
|
}
|
|
|
|
checkGetAccess(key);
|
|
Object value = estimatedData.get(key);
|
|
|
|
// A relation may be deserialized without a parent or key.
|
|
// Either way, make sure it's consistent.
|
|
if (value instanceof ParseRelation) {
|
|
((ParseRelation<?>) value).ensureParentAndKey(this, key);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
private void checkGetAccess(String key) {
|
|
if (!isDataAvailable(key)) {
|
|
throw new IllegalStateException(
|
|
"ParseObject has no data for '" + key + "'. Call fetchIfNeeded() to get the data.");
|
|
}
|
|
}
|
|
|
|
public boolean hasSameId(ParseObject other) {
|
|
synchronized (mutex) {
|
|
return this.getClassName() != null && this.getObjectId() != null
|
|
&& this.getClassName().equals(other.getClassName())
|
|
&& this.getObjectId().equals(other.getObjectId());
|
|
}
|
|
}
|
|
|
|
/* package */ void registerSaveListener(GetCallback<ParseObject> callback) {
|
|
synchronized (mutex) {
|
|
saveEvent.subscribe(callback);
|
|
}
|
|
}
|
|
|
|
/* package */ void unregisterSaveListener(GetCallback<ParseObject> callback) {
|
|
synchronized (mutex) {
|
|
saveEvent.unsubscribe(callback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a non-pointer is being created to allow additional initialization to occur.
|
|
*/
|
|
void setDefaultValues() {
|
|
if (needsDefaultACL() && ParseACL.getDefaultACL() != null) {
|
|
this.setACL(ParseACL.getDefaultACL());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether this object should get a default ACL. Override in subclasses to turn off
|
|
* default ACLs.
|
|
*/
|
|
boolean needsDefaultACL() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Registers the Parse-provided {@code ParseObject} subclasses. Do this here in a real method rather than
|
|
* as part of a static initializer because doing this in a static initializer can lead to
|
|
* deadlocks: https://our.intern.facebook.com/intern/tasks/?t=3508472
|
|
*/
|
|
/* package */ static void registerParseSubclasses() {
|
|
registerSubclass(ParseUser.class);
|
|
registerSubclass(ParseRole.class);
|
|
registerSubclass(ParseInstallation.class);
|
|
registerSubclass(ParseSession.class);
|
|
|
|
registerSubclass(ParsePin.class);
|
|
registerSubclass(EventuallyPin.class);
|
|
}
|
|
|
|
/* package */ static void unregisterParseSubclasses() {
|
|
unregisterSubclass(ParseUser.class);
|
|
unregisterSubclass(ParseRole.class);
|
|
unregisterSubclass(ParseInstallation.class);
|
|
unregisterSubclass(ParseSession.class);
|
|
|
|
unregisterSubclass(ParsePin.class);
|
|
unregisterSubclass(EventuallyPin.class);
|
|
}
|
|
|
|
/**
|
|
* Default name for pinning if not specified.
|
|
*
|
|
* @see #pin()
|
|
* @see #unpin()
|
|
*/
|
|
public static final String DEFAULT_PIN = "_default";
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAllInBackground(String, java.util.List, DeleteCallback)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects to be pinned
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static <T extends ParseObject> void pinAllInBackground(String name,
|
|
List<T> objects, SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(name, objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAllInBackground(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects to be pinned
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when pinning all completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> pinAllInBackground(final String name,
|
|
final List<T> objects) {
|
|
return pinAllInBackground(name, objects, true);
|
|
}
|
|
|
|
private static <T extends ParseObject> Task<Void> pinAllInBackground(final String name,
|
|
final List<T> objects, final boolean includeAllChildren) {
|
|
if (!Parse.isLocalDatastoreEnabled()) {
|
|
throw new IllegalStateException("Method requires Local Datastore. " +
|
|
"Please refer to `Parse#enableLocalDatastore(Context)`.");
|
|
}
|
|
|
|
Task<Void> task = Task.forResult(null);
|
|
|
|
// Resolve and persist unresolved users attached via ACL, similarly how we do in saveAsync
|
|
for (final ParseObject object : objects) {
|
|
task = task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
if (!object.isDataAvailable(KEY_ACL)) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
final ParseACL acl = object.getACL(false);
|
|
if (acl == null) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
ParseUser user = acl.getUnresolvedUser();
|
|
if (user == null || !user.isCurrentUser()) {
|
|
return Task.forResult(null);
|
|
}
|
|
|
|
return ParseUser.pinCurrentUserIfNeededAsync(user);
|
|
}
|
|
});
|
|
}
|
|
|
|
return task.onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
return Parse.getLocalDatastore().pinAllObjectsAsync(
|
|
name != null ? name : DEFAULT_PIN,
|
|
objects,
|
|
includeAllChildren);
|
|
}
|
|
}).onSuccessTask(new Continuation<Void, Task<Void>>() {
|
|
@Override
|
|
public Task<Void> then(Task<Void> task) throws Exception {
|
|
// Hack to emulate persisting current user on disk after a save like in ParseUser#saveAsync
|
|
// Note: This does not persist current user if it's a child object of `objects`, it probably
|
|
// should, but we can't unless we do something similar to #deepSaveAsync.
|
|
if (ParseCorePlugins.PIN_CURRENT_USER.equals(name)) {
|
|
return task;
|
|
}
|
|
for (ParseObject object : objects) {
|
|
if (object instanceof ParseUser) {
|
|
final ParseUser user = (ParseUser) object;
|
|
if (user.isCurrentUser()) {
|
|
return ParseUser.pinCurrentUserIfNeededAsync(user);
|
|
}
|
|
}
|
|
}
|
|
return task;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
* {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAll(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects to be pinned
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public static <T extends ParseObject> void pinAll(String name,
|
|
List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(pinAllInBackground(name, objects));
|
|
}
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAllInBackground(java.util.List, DeleteCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects to be pinned
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static <T extends ParseObject> void pinAllInBackground(List<T> objects,
|
|
SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(pinAllInBackground(DEFAULT_PIN, objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAllInBackground(java.util.List)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects to be pinned
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when pinning all completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> pinAllInBackground(List<T> objects) {
|
|
return pinAllInBackground(DEFAULT_PIN, objects);
|
|
}
|
|
|
|
/**
|
|
* Stores the objects and every object they point to in the local datastore, recursively. If
|
|
* those other objects have not been fetched from Parse, they will not be stored. However, if they
|
|
* have changed data, all of the changes will be retained. To get the objects back later, you can
|
|
* use {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on it.
|
|
*
|
|
* @see #unpinAll(java.util.List)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects to be pinned
|
|
* @throws ParseException
|
|
*/
|
|
public static <T extends ParseObject> void pinAll(List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(pinAllInBackground(DEFAULT_PIN, objects));
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(String, java.util.List, SaveCallback)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static <T extends ParseObject> void unpinAllInBackground(String name, List<T> objects,
|
|
DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name, objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning all completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> unpinAllInBackground(String name,
|
|
List<T> objects) {
|
|
if (!Parse.isLocalDatastoreEnabled()) {
|
|
throw new IllegalStateException("Method requires Local Datastore. " +
|
|
"Please refer to `Parse#enableLocalDatastore(Context)`.");
|
|
}
|
|
if (name == null) {
|
|
name = DEFAULT_PIN;
|
|
}
|
|
return Parse.getLocalDatastore().unpinAllObjectsAsync(name, objects);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param objects
|
|
* the objects
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public static <T extends ParseObject> void unpinAll(String name,
|
|
List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(unpinAllInBackground(name, objects));
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(java.util.List, SaveCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static <T extends ParseObject> void unpinAllInBackground(List<T> objects,
|
|
DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(DEFAULT_PIN, objects), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(java.util.List)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning all completes.
|
|
*/
|
|
public static <T extends ParseObject> Task<Void> unpinAllInBackground(List<T> objects) {
|
|
return unpinAllInBackground(DEFAULT_PIN, objects);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(java.util.List)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param objects
|
|
* the objects
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public static <T extends ParseObject> void unpinAll(List<T> objects) throws ParseException {
|
|
ParseTaskUtils.wait(unpinAllInBackground(DEFAULT_PIN, objects));
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static void unpinAllInBackground(String name, DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(name), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning all completes.
|
|
*/
|
|
public static Task<Void> unpinAllInBackground(String name) {
|
|
if (!Parse.isLocalDatastoreEnabled()) {
|
|
throw new IllegalStateException("Method requires Local Datastore. " +
|
|
"Please refer to `Parse#enableLocalDatastore(Context)`.");
|
|
}
|
|
if (name == null) {
|
|
name = DEFAULT_PIN;
|
|
}
|
|
return Parse.getLocalDatastore().unpinAllObjectsAsync(name);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(String, java.util.List)
|
|
*
|
|
* @param name
|
|
* the name
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public static void unpinAll(String name) throws ParseException {
|
|
ParseTaskUtils.wait(unpinAllInBackground(name));
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(java.util.List, SaveCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public static void unpinAllInBackground(DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinAllInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAllInBackground(java.util.List, SaveCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning all completes.
|
|
*/
|
|
public static Task<Void> unpinAllInBackground() {
|
|
return unpinAllInBackground(DEFAULT_PIN);
|
|
}
|
|
|
|
/**
|
|
* Removes the objects and every object they point to in the local datastore, recursively.
|
|
*
|
|
* @see #pinAll(java.util.List)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public static void unpinAll() throws ParseException {
|
|
ParseTaskUtils.wait(unpinAllInBackground());
|
|
}
|
|
|
|
/**
|
|
* Loads data from the local datastore into this object, if it has not been fetched from the
|
|
* server already. If the object is not stored in the local datastore, this method with do
|
|
* nothing.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
/* package */ <T extends ParseObject> Task<T> fetchFromLocalDatastoreAsync() {
|
|
if (!Parse.isLocalDatastoreEnabled()) {
|
|
throw new IllegalStateException("Method requires Local Datastore. " +
|
|
"Please refer to `Parse#enableLocalDatastore(Context)`.");
|
|
}
|
|
return Parse.getLocalDatastore().fetchLocallyAsync((T) this);
|
|
}
|
|
|
|
/**
|
|
* Loads data from the local datastore into this object, if it has not been fetched from the
|
|
* server already. If the object is not stored in the local datastore, this method with do
|
|
* nothing.
|
|
*/
|
|
public <T extends ParseObject> void fetchFromLocalDatastoreInBackground(GetCallback<T> callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(this.<T>fetchFromLocalDatastoreAsync(), callback);
|
|
}
|
|
|
|
/**
|
|
* Loads data from the local datastore into this object, if it has not been fetched from the
|
|
* server already. If the object is not stored in the local datastore, this method with throw a
|
|
* CACHE_MISS exception.
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public void fetchFromLocalDatastore() throws ParseException {
|
|
ParseTaskUtils.wait(fetchFromLocalDatastoreAsync());
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @see #unpinInBackground(String, DeleteCallback)
|
|
*
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public void pinInBackground(String name, SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(name), callback);
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when pinning completes.
|
|
*
|
|
* @see #unpinInBackground(String)
|
|
*/
|
|
public Task<Void> pinInBackground(String name) {
|
|
return pinAllInBackground(name, Collections.singletonList(this));
|
|
}
|
|
|
|
/* package */ Task<Void> pinInBackground(String name, boolean includeAllChildren) {
|
|
return pinAllInBackground(name, Collections.singletonList(this), includeAllChildren);
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @see #unpin(String)
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public void pin(String name) throws ParseException {
|
|
ParseTaskUtils.wait(pinInBackground(name));
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @see #unpinInBackground(DeleteCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public void pinInBackground(SaveCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(pinInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when pinning completes.
|
|
*
|
|
* @see #unpinInBackground()
|
|
* @see #DEFAULT_PIN
|
|
*/
|
|
public Task<Void> pinInBackground() {
|
|
return pinAllInBackground(DEFAULT_PIN, Arrays.asList(this));
|
|
}
|
|
|
|
/**
|
|
* Stores the object and every object it points to in the local datastore, recursively. If those
|
|
* other objects have not been fetched from Parse, they will not be stored. However, if they have
|
|
* changed data, all of the changes will be retained. To get the objects back later, you can use
|
|
* {@link ParseQuery#fromLocalDatastore()}, or you can create an unfetched pointer with
|
|
* {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on
|
|
* it.
|
|
*
|
|
* @see #unpin()
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @throws ParseException
|
|
*/
|
|
public void pin() throws ParseException {
|
|
ParseTaskUtils.wait(pinInBackground());
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @see #pinInBackground(String, SaveCallback)
|
|
*
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public void unpinInBackground(String name, DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(name), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning completes.
|
|
*
|
|
* @see #pinInBackground(String)
|
|
*/
|
|
public Task<Void> unpinInBackground(String name) {
|
|
return unpinAllInBackground(name, Arrays.asList(this));
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @see #pin(String)
|
|
*/
|
|
public void unpin(String name) throws ParseException {
|
|
ParseTaskUtils.wait(unpinInBackground(name));
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @see #pinInBackground(SaveCallback)
|
|
* @see #DEFAULT_PIN
|
|
*
|
|
* @param callback
|
|
* the callback
|
|
*/
|
|
public void unpinInBackground(DeleteCallback callback) {
|
|
ParseTaskUtils.callbackOnMainThreadAsync(unpinInBackground(), callback);
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @return A {@link bolts.Task} that is resolved when unpinning completes.
|
|
*
|
|
* @see #pinInBackground()
|
|
* @see #DEFAULT_PIN
|
|
*/
|
|
public Task<Void> unpinInBackground() {
|
|
return unpinAllInBackground(DEFAULT_PIN, Arrays.asList(this));
|
|
}
|
|
|
|
/**
|
|
* Removes the object and every object it points to in the local datastore, recursively.
|
|
*
|
|
* @see #pin()
|
|
* @see #DEFAULT_PIN
|
|
*/
|
|
public void unpin() throws ParseException {
|
|
ParseTaskUtils.wait(unpinInBackground());
|
|
}
|
|
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
writeToParcel(dest, new ParseObjectParcelEncoder(this));
|
|
}
|
|
|
|
/* package */ void writeToParcel(Parcel dest, ParseParcelEncoder encoder) {
|
|
synchronized (mutex) {
|
|
// Developer warnings.
|
|
ldsEnabledWhenParceling = Parse.isLocalDatastoreEnabled();
|
|
boolean saving = hasOutstandingOperations();
|
|
boolean deleting = isDeleting || isDeletingEventually > 0;
|
|
if (saving) {
|
|
Log.w(TAG, "About to parcel a ParseObject while a save / saveEventually operation is " +
|
|
"going on. If recovered from LDS, the unparceled object will be internally updated when " +
|
|
"these tasks end. If not, it will act as if these tasks have failed. This means that " +
|
|
"the subsequent call to save() will update again the same keys, and this is dangerous " +
|
|
"for certain operations, like increment(). To avoid inconsistencies, wait for operations " +
|
|
"to end before parceling.");
|
|
}
|
|
if (deleting) {
|
|
Log.w(TAG, "About to parcel a ParseObject while a delete / deleteEventually operation is " +
|
|
"going on. If recovered from LDS, the unparceled object will be internally updated when " +
|
|
"these tasks end. If not, it will assume it's not deleted, and might incorrectly " +
|
|
"return false for isDirty(). To avoid inconsistencies, wait for operations to end " +
|
|
"before parceling.");
|
|
}
|
|
// Write className and id first, regardless of state.
|
|
dest.writeString(getClassName());
|
|
String objectId = getObjectId();
|
|
dest.writeByte(objectId != null ? (byte) 1 : 0);
|
|
if (objectId != null) dest.writeString(objectId);
|
|
// Write state and other members
|
|
state.writeToParcel(dest, encoder);
|
|
dest.writeByte(localId != null ? (byte) 1 : 0);
|
|
if (localId != null) dest.writeString(localId);
|
|
dest.writeByte(isDeleted ? (byte) 1 : 0);
|
|
// Care about dirty changes and ongoing tasks.
|
|
ParseOperationSet set;
|
|
if (hasOutstandingOperations()) {
|
|
// There's more than one set. Squash the queue, creating copies
|
|
// to preserve the original queue when LDS is enabled.
|
|
set = new ParseOperationSet();
|
|
for (ParseOperationSet operationSet : operationSetQueue) {
|
|
ParseOperationSet copy = new ParseOperationSet(operationSet);
|
|
copy.mergeFrom(set);
|
|
set = copy;
|
|
}
|
|
} else {
|
|
set = operationSetQueue.getLast();
|
|
}
|
|
set.setIsSaveEventually(false);
|
|
set.toParcel(dest, encoder);
|
|
// Pass a Bundle to subclasses.
|
|
Bundle bundle = new Bundle();
|
|
onSaveInstanceState(bundle);
|
|
dest.writeBundle(bundle);
|
|
}
|
|
}
|
|
|
|
public final static Creator<ParseObject> CREATOR = new Creator<ParseObject>() {
|
|
@Override
|
|
public ParseObject createFromParcel(Parcel source) {
|
|
return ParseObject.createFromParcel(source, new ParseObjectParcelDecoder());
|
|
}
|
|
|
|
@Override
|
|
public ParseObject[] newArray(int size) {
|
|
return new ParseObject[size];
|
|
}
|
|
};
|
|
|
|
/* package */ static ParseObject createFromParcel(Parcel source, ParseParcelDecoder decoder) {
|
|
String className = source.readString();
|
|
String objectId = source.readByte() == 1 ? source.readString() : null;
|
|
// Create empty object (might be the same instance if LDS is enabled)
|
|
// and pass to decoder before unparceling child objects in State
|
|
ParseObject object = createWithoutData(className, objectId);
|
|
if (decoder instanceof ParseObjectParcelDecoder) {
|
|
((ParseObjectParcelDecoder) decoder).addKnownObject(object);
|
|
}
|
|
State state = State.createFromParcel(source, decoder);
|
|
object.setState(state);
|
|
if (source.readByte() == 1) object.localId = source.readString();
|
|
if (source.readByte() == 1) object.isDeleted = true;
|
|
// If object.ldsEnabledWhenParceling is true, we got this from OfflineStore.
|
|
// There is no need to restore operations in that case.
|
|
boolean restoreOperations = !object.ldsEnabledWhenParceling;
|
|
ParseOperationSet set = ParseOperationSet.fromParcel(source, decoder);
|
|
if (restoreOperations) {
|
|
for (String key : set.keySet()) {
|
|
ParseFieldOperation op = set.get(key);
|
|
object.performOperation(key, op); // Update ops and estimatedData
|
|
}
|
|
}
|
|
Bundle bundle = source.readBundle(ParseObject.class.getClassLoader());
|
|
object.onRestoreInstanceState(bundle);
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* Called when parceling this ParseObject.
|
|
* Subclasses can put values into the provided {@link Bundle} and receive them later
|
|
* {@link #onRestoreInstanceState(Bundle)}. Note that internal fields are already parceled by
|
|
* the framework.
|
|
*
|
|
* @param outState Bundle to host extra values
|
|
*/
|
|
protected void onSaveInstanceState(Bundle outState) {}
|
|
|
|
/**
|
|
* Called when unparceling this ParseObject.
|
|
* Subclasses can read values from the provided {@link Bundle} that were previously put
|
|
* during {@link #onSaveInstanceState(Bundle)}. At this point the internal state is already
|
|
* recovered.
|
|
*
|
|
* @param savedState Bundle to read the values from
|
|
*/
|
|
protected void onRestoreInstanceState(Bundle savedState) {}
|
|
|
|
}
|
|
|
|
// [1] Normally we should only construct the command from state when it's our turn in the
|
|
// taskQueue so that new objects will have an updated objectId from previous saves.
|
|
// We can't do this for save/deleteEventually since this will break the promise that we'll
|
|
// try to run the command eventually, since our process might die before it's our turn in
|
|
// the taskQueue.
|
|
// This seems like this will only be a problem for new objects that are saved &
|
|
// save/deleteEventually'd at the same time, as the first will create 2 objects and the second
|
|
// the delete might fail.
|