All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.easimart.EasimartObject Maven / Gradle / Ivy

package co.easimart;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
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.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;

import bolts.Capture;
import bolts.Continuation;
import bolts.Task;

/**
 * The {@code EasimartObject} is a local representation of data that can be saved and retrieved from
 * the Easimart cloud.
 * 

* The basic workflow for creating new data is to construct a new {@code EasimartObject}, use * {@link #put(String, Object)} to fill it with data, and then use {@link #saveInBackground()} to * persist to the cloud. *

* The basic workflow for accessing existing data is to use a {@link EasimartQuery} to specify which * existing data to retrieve. */ public class EasimartObject { /* package */ static String server = "https://api.parse.com"; private static final String AUTO_CLASS_NAME = "_Automatic"; /* package */ static final String VERSION_NAME = "1.11.1-SNAPSHOT"; /* 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 EasimartObject}s locally. */ private static final String KEY_COMPLETE = "__complete"; private static final String KEY_OPERATIONS = "__operations"; /* 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 final Map, String> classNames = new ConcurrentHashMap<>(); private static final Map> objectTypes = new ConcurrentHashMap<>(); private static EasimartObjectController getObjectController() { return EasimartCorePlugins.getInstance().getObjectController(); } private static LocalIdManager getLocalIdManager() { return EasimartCorePlugins.getInstance().getLocalIdManager(); } /** package */ static class State { public static Init newBuilder(String className) { if ("_User".equals(className)) { return new EasimartUser.State.Builder(); } return new Builder(className); } /** package */ static abstract class Init { private final String className; private String objectId; private long createdAt = -1; private long updatedAt = -1; private boolean isComplete; /* package */ Map 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(); for (String key : state.keySet()) { serverData.put(key, state.get(key)); } isComplete = state.isComplete(); } /* package */ abstract T self(); /* package */ abstract 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); return self(); } public T remove(String key) { serverData.remove(key); return self(); } public T clear() { objectId = null; createdAt = -1; updatedAt = -1; isComplete = false; serverData.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)); } return self(); } public T apply(EasimartOperationSet operations) { for (String key : operations.keySet()) { EasimartFieldOperation 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 { 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 serverData; 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; } @SuppressWarnings("unchecked") public > 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 keySet() { return serverData.keySet(); } @Override public String toString() { return String.format(Locale.US, "%s@%s[" + "className=%s, objectId=%s, createdAt=%d, updatedAt=%d, isComplete=%s, " + "serverData=%s]", getClass().getName(), Integer.toHexString(hashCode()), className, objectId, createdAt, updatedAt, isComplete, serverData); } } /* package */ final Object mutex = new Object(); /* package */ final TaskQueue taskQueue = new TaskQueue(); private State state; /* package */ final LinkedList operationSetQueue; // Cached State private final Map estimatedData; private String localId; private final EasimartMulticastDelegate saveEvent = new EasimartMulticastDelegate<>(); /* package */ boolean isDeleted; //TODO (grantland): Derive this off the EventuallyPins as opposed to +/- count. /* package */ int isDeletingEventually; private static final ThreadLocal isCreatingPointerForObjectId = new ThreadLocal() { @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 EasimartClassName} annotation on the subclass. */ protected EasimartObject() { this(AUTO_CLASS_NAME); } /** * Constructs a new {@code EasimartObject} with no data in it. A {@code EasimartObject} constructed in * this way will not have an objectId and will not persist to the database until {@link #save()} * is called. *

* Class names must be alphanumerical plus underscore, and start with a letter. It is recommended * to name classes in PascalCaseLikeThis. * * @param theClassName * The className for this {@code EasimartObject}. */ public EasimartObject(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 Easimart class name when creating a new EasimartObject."); } if (AUTO_CLASS_NAME.equals(theClassName)) { theClassName = getClassName(this.getClass()); } // If this is supposed to be created by a factory but wasn't, throw an exception. if (this.getClass().equals(EasimartObject.class) && objectTypes.containsKey(theClassName) && !objectTypes.get(theClassName).isInstance(this)) { throw new IllegalArgumentException( "You must create this type of EasimartObject using EasimartObject.create() or the proper subclass."); } // If this is an unregistered subclass, throw an exception. if (!this.getClass().equals(EasimartObject.class) && !this.getClass().equals(objectTypes.get(theClassName))) { throw new IllegalArgumentException( "You must register this EasimartObject subclass before instantiating it."); } operationSetQueue = new LinkedList<>(); operationSetQueue.add(new EasimartOperationSet()); 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 = Easimart.getLocalDatastore(); if (store != null) { store.registerNewObject(this); } } /** * Creates a new {@code EasimartObject} based upon a class name. If the class name is a special type * (e.g. for {@code EasimartUser}), then the appropriate type of {@code EasimartObject} is returned. * * @param className * The class of object to create. * @return A new {@code EasimartObject} for the given class name. */ public static EasimartObject create(String className) { if (objectTypes.containsKey(className)) { try { return objectTypes.get(className).newInstance(); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } throw new RuntimeException("Failed to create instance of subclass.", e); } } return new EasimartObject(className); } /** * Creates a new {@code EasimartObject} based upon a subclass type. Note that the object will be * created based upon the {@link EasimartClassName} of the given subclass type. For example, calling * create(EasimartUser.class) may create an instance of a custom subclass of {@code EasimartUser}. * * @param subclass * The class of object to create. * @return A new {@code EasimartObject} based upon the class name of the given subclass type. */ @SuppressWarnings("unchecked") public static T create(Class subclass) { return (T) create(getClassName(subclass)); } /** * Creates a reference to an existing {@code EasimartObject} for use in creating associations between * {@code EasimartObject}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 EasimartObject} without data. */ public static EasimartObject createWithoutData(String className, String objectId) { OfflineStore store = Easimart.getLocalDatastore(); try { if (objectId == null) { isCreatingPointerForObjectId.set(NEW_OFFLINE_OBJECT_ID_PLACEHOLDER); } else { isCreatingPointerForObjectId.set(objectId); } EasimartObject 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 EasimartObject 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 EasimartObject} for use in creating associations between * {@code EasimartObject}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 EasimartObject} subclass to create. * @param objectId * The object id for the referenced object. * @return A {@code EasimartObject} without data. */ @SuppressWarnings({"unused", "unchecked"}) public static T createWithoutData(Class subclass, String objectId) { return (T) createWithoutData(getClassName(subclass), objectId); } private static boolean isAccessible(Member m) { return Modifier.isPublic(m.getModifiers()) || (m.getDeclaringClass().getPackage().getName().equals("co.easimart") && !Modifier.isPrivate(m.getModifiers()) && !Modifier.isProtected(m.getModifiers())); } /** * Registers a custom subclass type with the Easimart SDK, enabling strong-typing of those * {@code EasimartObject}s whenever they appear. Subclasses must specify the {@link EasimartClassName} * annotation and have a default constructor. * * @param subclass * The subclass type to register. */ public static void registerSubclass(Class subclass) { String className = getClassName(subclass); if (className == null) { throw new IllegalArgumentException("No EasimartClassName annotation provided on " + subclass); } if (subclass.getDeclaredConstructors().length > 0) { try { if (!isAccessible(subclass.getDeclaredConstructor())) { throw new IllegalArgumentException("Default constructor for " + subclass + " is not accessible."); } } catch (NoSuchMethodException e) { throw new IllegalArgumentException("No default constructor provided for " + subclass); } } Class oldValue = objectTypes.get(className); if (oldValue != null && subclass.isAssignableFrom(oldValue)) { // The old class was already more descendant than the new subclass type. No-op. return; } objectTypes.put(className, subclass); if (oldValue != null && !subclass.equals(oldValue)) { if (className.equals(getClassName(EasimartUser.class))) { EasimartUser.getCurrentUserController().clearFromMemory(); } else if (className.equals(getClassName(EasimartInstallation.class))) { EasimartInstallation.getCurrentInstallationController().clearFromMemory(); } } } /* package for tests */ static void unregisterSubclass(Class subclass) { unregisterSubclass(getClassName(subclass)); } /* package for tests */ static void unregisterSubclass(String className) { objectTypes.remove(className); } /** * Adds a task to the queue for all of the given objects. */ static Task enqueueForAll(final List objects, Continuation> taskStart) { // The task that will be complete when all of the child queues indicate they're ready to start. final Task.TaskCompletionSource readyToStart = Task.create(); // 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 locks = new ArrayList<>(objects.size()); for (EasimartObject obj : objects) { locks.add(obj.taskQueue.getLock()); } LockSet lock = new LockSet(locks); lock.lock(); try { // The task produced by TaskStart final Task 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> childTasks = new ArrayList<>(); for (EasimartObject obj : objects) { obj.taskQueue.enqueue(new Continuation>() { @Override public Task then(Task 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() { @Override public Void then(Task task) throws Exception { readyToStart.setResult(null); return null; } }); return fullTask; } finally { lock.unlock(); } } /** * Converts a {@code EasimartObject.State} to a {@code EasimartObject}. * * @param state * The {@code EasimartObject.State} to convert from. * @return A {@code EasimartObject} instance. */ /* package */ static T from(EasimartObject.State state) { @SuppressWarnings("unchecked") T object = (T) EasimartObject.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 EasimartObject} based on data from the Easimart server. * * @param json * The object's data. * @param defaultClassName * The className of the object, if none is in the JSON. * @param isComplete * {@code true} if this is all of the data on the server for the object. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, boolean isComplete) { return fromJSON(json, defaultClassName, isComplete, EasimartDecoder.get()); } /** * Creates a new {@code EasimartObject} based on data from the Easimart server. * * @param json * The object's data. * @param defaultClassName * The className of the object, if none is in the JSON. * @param isComplete * {@code true} if this is all of the data on the server for the object. * @param decoder * Delegate for knowing how to decode the values in the JSON. */ /* package */ static T fromJSON(JSONObject json, String defaultClassName, boolean isComplete, EasimartDecoder decoder) { String className = json.optString(KEY_CLASS_NAME, defaultClassName); if (className == null) { return null; } String objectId = json.optString(KEY_OBJECT_ID, null); @SuppressWarnings("unchecked") T object = (T) EasimartObject.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 Easimart Object * * Method is used by parse server webhooks implementation to create a * new {@code EasimartObject} from the incoming json payload. The method is different from * {@link #fromJSON(JSONObject, String, boolean)} ()} in that it calls * {@link #build(JSONObject, EasimartDecoder)} 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 EasimartFieldOperations} separately. * * @param json * The object's data. * @param decoder * Delegate for knowing how to decode the values in the JSON. */ /* package */ static T fromJSONPayload( JSONObject json, EasimartDecoder decoder) { String className = json.optString(KEY_CLASS_NAME); if (className == null || EasimartTextUtils.isEmpty(className)) { return null; } String objectId = json.optString(KEY_OBJECT_ID, null); @SuppressWarnings("unchecked") T object = (T) EasimartObject.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 && !EasimartTextUtils.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 EasimartObject}, 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 EasimartObject}, 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 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(EasimartObject other) { synchronized (mutex) { EasimartOperationSet operations = other.operationSetQueue.getFirst(); for (String key : operations.keySet()) { performOperation(key, operations.get(key)); } } } /* package */ void mergeFromObject(EasimartObject 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 EasimartUser instance that's calling signUp(). This // doesn't make any sense and we should probably remove that code in EasimartUser. // 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 EasimartObject} which has been fetched. */ private Map collectFetchedObjects() { final Map fetchedObjects = new HashMap<>(); EasimartTraverser traverser = new EasimartTraverser() { @Override protected boolean visit(Object object) { if (object instanceof EasimartObject) { EasimartObject easimartObject = (EasimartObject) object; State state = easimartObject.getState(); if (state.objectId() != null && state.isComplete()) { fetchedObjects.put(state.objectId(), easimartObject); } } return true; } }; traverser.traverse(estimatedData); return fetchedObjects; } /** * Helper method called by {@link #fromJSONPayload(JSONObject, EasimartDecoder)} * * The method helps webhooks implementation to build Easimart object from raw JSON payload. * It is different from {@link #mergeFromServer(State, JSONObject, EasimartDecoder, 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 EasimartFieldOperations} differently. * * @param json : JSON object to be converted to Easimart object * @param decoder : Decoder to be used for Decoding JSON */ /* package */ void build(JSONObject json, EasimartDecoder 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(EasimartDateFormat.getInstance().parse(json.getString(key))); continue; } if (key.equals(KEY_UPDATED_AT)) { builder.updatedAt(EasimartDateFormat.getInstance().parse(json.getString(key))); continue; } Object value = json.get(key); Object decodedObject = decoder.decode(value); if (decodedObject instanceof EasimartFieldOperation) { performOperation(key, (EasimartFieldOperation)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, EasimartOperationSet, EasimartEncoder) */ /* package */ State mergeFromServer( State state, JSONObject json, EasimartDecoder 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 EasimartObject __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(EasimartDateFormat.getInstance().parse(json.getString(key))); continue; } if (key.equals(KEY_UPDATED_AT)) { builder.updatedAt(EasimartDateFormat.getInstance().parse(json.getString(key))); continue; } if (key.equals(KEY_ACL)) { EasimartACL acl = EasimartACL.createACLFromJSONObject(json.getJSONObject(key), decoder); builder.put(KEY_ACL, acl); continue; } Object value = json.get(key); 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, EasimartDecoder) */ /* package */ JSONObject toRest(EasimartEncoder encoder) { State state; List 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++) { EasimartOperationSet original = operationSetQueue.get(i); EasimartOperationSet copy = new EasimartOperationSet(original); operationSetQueueCopy.add(copy); } } return toRest(state, operationSetQueueCopy, encoder); } /* package */ JSONObject toRest( State state, List operationSetQueue, EasimartEncoder 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, EasimartDateFormat.getInstance().format(new Date(state.createdAt()))); } if (state.updatedAt() > 0) { json.put(KEY_UPDATED_AT, EasimartDateFormat.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 Easimart. json.put(KEY_COMPLETE, state.isComplete()); json.put(KEY_IS_DELETING_EVENTUALLY, isDeletingEventually); // Operation Set Queue JSONArray operations = new JSONArray(); for (EasimartOperationSet 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(EasimartEncoder) */ /* package */ void mergeREST(State state, JSONObject json, EasimartDecoder decoder) { ArrayList saveEventuallyOperationSets = new ArrayList<>(); synchronized (mutex) { try { boolean isComplete = json.getBoolean(KEY_COMPLETE); isDeletingEventually = EasimartJSONUtils.getInt(json, Arrays.asList( KEY_IS_DELETING_EVENTUALLY, KEY_IS_DELETING_EVENTUALLY_OLD )); JSONArray operations = json.getJSONArray(KEY_OPERATIONS); { EasimartOperationSet newerOperations = currentOperations(); operationSetQueue.clear(); // Add and enqueue any saveEventually operations, roll forward any other operation sets // (operation sets here are generally failed/incomplete saves). EasimartOperationSet current = null; for (int i = 0; i < operations.length(); i++) { JSONObject operationSetJSON = operations.getJSONObject(i); EasimartOperationSet operationSet = EasimartOperationSet.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 = EasimartDateFormat.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 = EasimartJSONUtils.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 (EasimartOperationSet 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 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 EasimartObject} 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 EasimartObject} yourself and wish * to recreate it, you can use this to recreate the {@code EasimartObject} exactly. */ public void setObjectId(String newObjectId) { synchronized (mutex) { String oldObjectId = state.objectId(); if (EasimartTextUtils.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 = Easimart.getLocalDatastore(); if (store != null) { store.updateObjectId(this, oldObjectId, newObjectId); } if (localId != null) { getLocalIdManager().setObjectId(localId, newObjectId); localId = null; } } } private EasimartRESTObjectCommand currentSaveEventuallyCommand( EasimartOperationSet operations, EasimartEncoder objectEncoder, String sessionToken) throws EasimartException { 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); EasimartRESTObjectCommand command = EasimartRESTObjectCommand.saveObjectCommand( state, objectJSON, sessionToken); command.enableRetrying(); return command; } /** * Converts a {@code EasimartObject} to a JSON representation for saving to Easimart. * *

   * {
   *   data: { // objectId plus any EasimartFieldOperations },
   *   classname: class name for the object
   * }
   * 
* * updatedAt and createdAt are not included. only dirty keys are represented in the data. * * @see #mergeFromServer(State state, org.json.JSONObject, EasimartDecoder, boolean) */ // Currently only used by saveEventually /* package */ JSONObject toJSONObjectForSaving( T state, EasimartOperationSet operations, EasimartEncoder objectEncoder) { JSONObject objectJSON = new JSONObject(); try { // Serialize the data for (String key : operations.keySet()) { EasimartFieldOperation 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 EasimartUser.signUp/resolveLaziness // to controllers /* package */ Task handleSaveResultAsync( final JSONObject result, final EasimartOperationSet operationsBeforeSave) { EasimartObject.State newState = null; if (result != null) { // Success synchronized (mutex) { final Map fetchedObjects = collectFetchedObjects(); EasimartDecoder decoder = new KnownEasimartObjectDecoder(fetchedObjects); newState = EasimartObjectCoder.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 handleSaveResultAsync( final EasimartObject.State result, final EasimartOperationSet operationsBeforeSave) { Task task = Task.forResult((Void) 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 opIterator = operationSetQueue.listIterator(operationSetQueue.indexOf(operationsBeforeSave)); opIterator.next(); opIterator.remove(); if (!success) { // Merge the data from the failed save into the next save. EasimartOperationSet 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 = Easimart.getLocalDatastore(); if (store != null) { task = task.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return store.fetchLocallyAsync(EasimartObject.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() { @Override public Void then(Task 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>() { @Override public Task then(Task task) throws Exception { return store.updateDataForObjectAsync(EasimartObject.this); } }); } task = task.onSuccess(new Continuation() { @Override public Void then(Task task) throws Exception { saveEvent.invoke(EasimartObject.this, null); return null; } }); return task; } /* package */ EasimartOperationSet startSave() { synchronized (mutex) { EasimartOperationSet currentOperations = currentOperations(); operationSetQueue.addLast(new EasimartOperationSet()); 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 EasimartException * Throws an exception if the server is inaccessible. */ public final void save() throws EasimartException { EasimartTaskUtils.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 saveInBackground() { return EasimartUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final EasimartUser 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 EasimartACL acl = getACL(false); if (acl == null) { return Task.forResult(null); } final EasimartUser user = acl.getUnresolvedUser(); if (user == null || !user.isCurrentUser()) { return Task.forResult(null); } return user.saveAsync(null).onSuccess(new Continuation() { @Override public String then(Task task) throws Exception { if (acl.hasUnresolvedUser()) { throw new IllegalStateException("ACL has an unresolved EasimartUser. " + "Save or sign up before attempting to serialize the ACL."); } return user.getSessionToken(); } }); } }).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final String sessionToken = task.getResult(); return saveAsync(sessionToken); } }); } /* package */ Task saveAsync(final String sessionToken) { return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return saveAsync(sessionToken, toAwait); } }); } /* package */ Task saveAsync(final String sessionToken, final Task toAwait) { if (!isDirty()) { return Task.forResult(null); } final EasimartOperationSet operations; synchronized (mutex) { updateBeforeSave(); validateSave(); operations = startSave(); } Task 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.waitFor(toAwait) ).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final Map fetchedObjects = collectFetchedObjects(); EasimartDecoder decoder = new KnownEasimartObjectDecoder(fetchedObjects); return getObjectController().saveAsync(getState(), operations, sessionToken, decoder); } }).continueWithTask(new Continuation>() { @Override public Task then(final Task saveTask) throws Exception { EasimartObject.State result = saveTask.getResult(); return handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (task.isFaulted() || task.isCancelled()) { return task; } // We still want to propagate saveTask errors return saveTask.makeVoid(); } }); } }); } // Currently only used by EasimartPinningEventuallyQueue for saveEventually due to the limitation in // EasimartCommandCache that it can only return JSONObject result. /* package */ Task saveAsync( EasimartHttpClient client, final EasimartOperationSet operationSet, String sessionToken) throws EasimartException { final EasimartRESTCommand 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) { EasimartTaskUtils.callbackOnMainThreadAsync(saveInBackground(), callback); } /* package */ void validateSaveEventually() throws EasimartException { // do nothing } /** * Saves this object to the server at some unspecified time in the future, even if Easimart 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 Easimart. 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) { EasimartTaskUtils.callbackOnMainThreadAsync(saveEventually(), callback); } /** * Saves this object to the server at some unspecified time in the future, even if Easimart 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 Easimart. 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 saveEventually() { if (!isDirty()) { Easimart.getEventuallyQueue().fakeObjectUpdate(); return Task.forResult(null); } final EasimartOperationSet operationSet; final EasimartRESTCommand command; final Task runEventuallyTask; synchronized (mutex) { updateBeforeSave(); try { validateSaveEventually(); } catch (EasimartException 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 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 = EasimartUser.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 (EasimartObject object : unsavedChildren) { object.saveEventually(); } } catch (EasimartException exception) { throw new IllegalStateException("Unable to saveEventually.", exception); } } // We cannot modify the taskQueue inside synchronized (mutex). EasimartEventuallyQueue cache = Easimart.getEventuallyQueue(); runEventuallyTask = cache.enqueueEventuallyAsync(command, EasimartObject.this); enqueueSaveEventuallyOperationAsync(operationSet); // Release the extra retained local ids. command.releaseLocalIds(); Task handleSaveResultTask; if (Easimart.isLocalDatastoreEnabled()) { // EasimartPinningEventuallyQueue calls handleSaveEventuallyResultAsync directly. handleSaveResultTask = runEventuallyTask.makeVoid(); } else { handleSaveResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { JSONObject json = task.getResult(); return handleSaveEventuallyResultAsync(json, operationSet); } }); } return handleSaveResultTask; } /** * Enqueues the saveEventually EasimartOperationSet in {@link #taskQueue}. */ private Task enqueueSaveEventuallyOperationAsync(final EasimartOperationSet operationSet) { if (!operationSet.isSaveEventually()) { throw new IllegalStateException( "This should only be used to enqueue saveEventually operation sets"); } return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { EasimartEventuallyQueue cache = Easimart.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 handleSaveEventuallyResultAsync( JSONObject json, EasimartOperationSet operationSet) { final boolean success = json != null; Task handleSaveResultTask = handleSaveResultAsync(json, operationSet); return handleSaveResultTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (success) { Easimart.getEventuallyQueue() .notifyTestHelper(EasimartCommandCache.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 Easimart 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 Easimart. 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) { EasimartTaskUtils.callbackOnMainThreadAsync(deleteEventually(), callback); } /** * Deletes this object from the server at some unspecified time in the future, even if Easimart 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 Easimart. 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 deleteEventually() { final EasimartRESTCommand command; final Task runEventuallyTask; synchronized (mutex) { validateDelete(); isDeletingEventually += 1; String localId = null; if (getObjectId() == null) { localId = getOrCreateLocalId(); } // TODO(grantland): Convert to async final String sessionToken = EasimartUser.getCurrentSessionToken(); // See [1] command = EasimartRESTObjectCommand.deleteObjectCommand( getState(), sessionToken); command.enableRetrying(); command.setLocalId(localId); runEventuallyTask = Easimart.getEventuallyQueue().enqueueEventuallyAsync(command, EasimartObject.this); } Task handleDeleteResultTask; if (Easimart.isLocalDatastoreEnabled()) { // EasimartPinningEventuallyQueue calls handleDeleteEventuallyResultAsync directly. handleDeleteResultTask = runEventuallyTask.makeVoid(); } else { handleDeleteResultTask = runEventuallyTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return handleDeleteEventuallyResultAsync(); } }); } return handleDeleteResultTask; } /** * Handles the result of {@code deleteEventually}. * * Should only be called on success. */ /* package */ Task handleDeleteEventuallyResultAsync() { synchronized (mutex) { isDeletingEventually -= 1; } Task handleDeleteResultTask = handleDeleteResultAsync(); return handleDeleteResultTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { Easimart.getEventuallyQueue() .notifyTestHelper(EasimartCommandCache.TestHelper.OBJECT_REMOVED); return task; } }); } /** * Handles the result of {@code fetch}. * * Should only be called on success. */ /* package */ Task handleFetchResultAsync(final EasimartObject.State result) { Task task = Task.forResult((Void) 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 = Easimart.getLocalDatastore(); if (store != null) { task = task.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return store.fetchLocallyAsync(EasimartObject.this).makeVoid(); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // Catch CACHE_MISS if (task.getError() instanceof EasimartException && ((EasimartException)task.getError()).getCode() == EasimartException.CACHE_MISS) { return null; } return task; } }); } task = task.onSuccessTask(new Continuation>() { @Override public Task then(Task 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>() { @Override public Task then(Task task) throws Exception { return store.updateDataForObjectAsync(EasimartObject.this); } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // Catch CACHE_MISS if (task.getError() instanceof EasimartException && ((EasimartException) task.getError()).getCode() == EasimartException.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 EasimartException * Throws an exception if the server is inaccessible. * * @deprecated Please use {@link #fetch()} instead. */ @Deprecated public final void refresh() throws EasimartException { 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) { EasimartTaskUtils.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 EasimartException * Throws an exception if the server is inaccessible. * @return The {@code EasimartObject} that was fetched. */ public T fetch() throws EasimartException { return EasimartTaskUtils.wait(this.fetchInBackground()); } @SuppressWarnings("unchecked") /* package */ Task fetchAsync( final String sessionToken, Task toAwait) { return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { State state; Map fetchedObjects; synchronized (mutex) { state = getState(); fetchedObjects = collectFetchedObjects(); } EasimartDecoder decoder = new KnownEasimartObjectDecoder(fetchedObjects); return getObjectController().fetchAsync(state, sessionToken, decoder); } }).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { EasimartObject.State result = task.getResult(); return handleFetchResultAsync(result); } }).onSuccess(new Continuation() { @Override public T then(Task task) throws Exception { return (T) EasimartObject.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 Task fetchInBackground() { return EasimartUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final String sessionToken = task.getResult(); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task 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 void fetchInBackground(GetCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(this.fetchInBackground(), callback); } /** * If this {@code EasimartObject} 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 Task fetchIfNeededInBackground() { if (isDataAvailable()) { return Task.forResult((T) this); } return EasimartUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final String sessionToken = task.getResult(); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { if (isDataAvailable()) { return Task.forResult((T) EasimartObject.this); } return fetchAsync(sessionToken, toAwait); } }); } }); } /** * If this {@code EasimartObject} has not been fetched (i.e. {@link #isDataAvailable()} returns {@code false}), * fetches this object with the data from the server. * * @throws EasimartException * Throws an exception if the server is inaccessible. * @return The fetched {@code EasimartObject}. */ public T fetchIfNeeded() throws EasimartException { return EasimartTaskUtils.wait(this.fetchIfNeededInBackground()); } /** * If this {@code EasimartObject} 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 void fetchIfNeededInBackground(GetCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(this.fetchIfNeededInBackground(), callback); } // Validates the delete method /* package */ void validateDelete() { // do nothing } private Task deleteAsync(final String sessionToken, Task toAwait) { validateDelete(); return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (state.objectId() == null) { return task.cast(); // no reason to call delete since it doesn't exist } return deleteAsync(sessionToken); } }).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return handleDeleteResultAsync(); } }); } //TODO (grantland): I'm not sure we want direct access to this. All access to `delete` should // enqueue on the taskQueue... /* package */ Task deleteAsync(String sessionToken) throws EasimartException { return getObjectController().deleteAsync(getState(), sessionToken); } /** * Handles the result of {@code delete}. * * Should only be called on success. */ /* package */ Task handleDeleteResultAsync() { Task task = Task.forResult(null); synchronized (mutex) { isDeleted = true; } final OfflineStore store = Easimart.getLocalDatastore(); if (store != null) { task = task.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { synchronized (mutex) { if (isDeleted) { store.unregisterObject(EasimartObject.this); return store.deleteDataForObjectAsync(EasimartObject.this); } else { return store.updateDataForObjectAsync(EasimartObject.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 deleteInBackground() { return EasimartUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final String sessionToken = task.getResult(); return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return deleteAsync(sessionToken, toAwait); } }); } }); } /** * Deletes this object on the server. This does not delete or destroy the object locally. * * @throws EasimartException * Throws an error if the object does not exist or if the internet fails. */ public final void delete() throws EasimartException { EasimartTaskUtils.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) { EasimartTaskUtils.callbackOnMainThreadAsync(deleteInBackground(), callback); } /** * This deletes all of the objects from the given List. */ private static Task deleteAllAsync( final List 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 uniqueObjects = new ArrayList<>(objectCount); final HashSet idSet = new HashSet<>(); for (int i = 0; i < objectCount; i++) { EasimartObject obj = objects.get(i); if (!idSet.contains(obj.getObjectId())) { idSet.add(obj.getObjectId()); uniqueObjects.add(obj); } } return enqueueForAll(uniqueObjects, new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return deleteAllAsync(uniqueObjects, sessionToken, toAwait); } }); } private static Task deleteAllAsync( final List uniqueObjects, final String sessionToken, Task toAwait) { return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { int objectCount = uniqueObjects.size(); List states = new ArrayList<>(objectCount); for (int i = 0; i < objectCount; i++) { EasimartObject object = uniqueObjects.get(i); object.validateDelete(); states.add(object.getState()); } List> batchTasks = getObjectController().deleteAllAsync(states, sessionToken); List> tasks = new ArrayList<>(objectCount); for (int i = 0; i < objectCount; i++) { Task batchTask = batchTasks.get(i); final T object = uniqueObjects.get(i); tasks.add(batchTask.onSuccessTask(new Continuation>() { @Override public Task then(final Task batchTask) throws Exception { return object.handleDeleteResultAsync().continueWithTask(new Continuation>() { @Override public Task then(Task 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 EasimartException * Throws an exception if the server returns an error or is inaccessible. */ public static void deleteAll(List objects) throws EasimartException { EasimartTaskUtils.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 void deleteAllInBackground(List objects, DeleteCallback callback) { EasimartTaskUtils.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 Task deleteAllInBackground(final List objects) { return EasimartUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task 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 EasimartObject}s into. * @param dirtyFiles * The array to collect the {@link EasimartFile}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 dirtyChildren, final Collection dirtyFiles, final Set alreadySeen, final Set alreadySeenNew) { new EasimartTraverser() { @Override protected boolean visit(Object node) { // If it's a file, then add it to the list if it's dirty. if (node instanceof EasimartFile) { if (dirtyFiles == null) { return true; } EasimartFile file = (EasimartFile) node; if (file.getUrl() == null) { dirtyFiles.add(file); } return true; } // If it's anything other than a file, then just continue; if (!(node instanceof EasimartObject)) { return true; } if (dirtyChildren == null) { return true; } // For files, we need to handle recursion manually to find cycles of new objects. EasimartObject object = (EasimartObject) node; Set seen = alreadySeen; Set 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 dirtyChildren, Collection dirtyFiles) { collectDirtyChildren(node, dirtyChildren, dirtyFiles, new HashSet(), new HashSet()); } /** * Returns {@code true} if this object can be serialized for saving. */ private boolean canBeSerialized() { synchronized (mutex) { final Capture 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 EasimartTraverser() { @Override protected boolean visit(Object value) { if (value instanceof EasimartFile) { EasimartFile file = (EasimartFile) value; if (file.isDirty()) { result.set(false); } } if (value instanceof EasimartObject) { EasimartObject object = (EasimartObject) value; if (object.getObjectId() == null) { result.set(false); } } // Continue to traverse only if it can still be serialized. return result.get(); } }.setYieldRoot(false).setTraverseEasimartObjects(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 deepSaveAsync(final Object object, final String sessionToken) { Set objects = new HashSet<>(); Set files = new HashSet<>(); collectDirtyChildren(object, objects, files); // This has to happen separately from everything else because EasimartUser.save() is // special-cased to work for lazy users, but new users can't be created by // EasimartMultiCommand's regular save. Set users = new HashSet<>(); for (EasimartObject o : objects) { if (o instanceof EasimartUser) { EasimartUser user = (EasimartUser) o; if (user.isLazy()) { users.add((EasimartUser) 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> tasks = new ArrayList<>(); for (EasimartFile file : files) { tasks.add(file.saveAsync(sessionToken, null, null)); } Task filesTask = Task.whenAll(tasks).continueWith(new Continuation() { @Override public Void then(Task 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 EasimartUser user : users) { tasks.add(user.saveAsync(sessionToken)); } Task usersTask = Task.whenAll(tasks).continueWith(new Continuation() { @Override public Void then(Task task) throws Exception { usersComplete.set(true); return null; } }); final Capture> remaining = new Capture<>(objects); Task objectsTask = Task.forResult(null).continueWhile(new Callable() { @Override public Boolean call() throws Exception { return remaining.get().size() > 0; } }, new Continuation>() { @Override public Task then(Task 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 current = new ArrayList<>(); final Set nextBatch = new HashSet<>(); for (EasimartObject 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 EasimartObject with a relation to a cycle."); } // Package all save commands together if (current.size() == 0) { return Task.forResult(null); } return enqueueForAll(current, new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return saveAllAsync(current, sessionToken, toAwait); } }); } }); return Task.whenAll(Arrays.asList(filesTask, usersTask, objectsTask)); } private static Task saveAllAsync( final List uniqueObjects, final String sessionToken, Task toAwait) { return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { int objectCount = uniqueObjects.size(); List states = new ArrayList<>(objectCount); List operationsList = new ArrayList<>(objectCount); List decoders = new ArrayList<>(objectCount); for (int i = 0; i < objectCount; i++) { EasimartObject object = uniqueObjects.get(i); object.updateBeforeSave(); object.validateSave(); states.add(object.getState()); operationsList.add(object.startSave()); final Map fetchedObjects = object.collectFetchedObjects(); decoders.add(new KnownEasimartObjectDecoder(fetchedObjects)); } List> batchTasks = getObjectController().saveAllAsync( states, operationsList, sessionToken, decoders); List> tasks = new ArrayList<>(objectCount); for (int i = 0; i < objectCount; i++) { Task batchTask = batchTasks.get(i); final T object = uniqueObjects.get(i); final EasimartOperationSet operations = operationsList.get(i); tasks.add(batchTask.continueWithTask(new Continuation>() { @Override public Task then(final Task batchTask) throws Exception { EasimartObject.State result = batchTask.getResult(); // will be null on failure return object.handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { @Override public Task then(Task 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 EasimartException * Throws an exception if the server returns an error or is inaccessible. */ public static void saveAll(List objects) throws EasimartException { EasimartTaskUtils.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 void saveAllInBackground(List objects, SaveCallback callback) { EasimartTaskUtils.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 Task saveAllInBackground(final List objects) { return EasimartUser.getCurrentUserAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final EasimartUser 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 (EasimartObject object : objects) { if (!object.isDataAvailable(KEY_ACL)) { continue; } final EasimartACL acl = object.getACL(false); if (acl == null) { continue; } final EasimartUser 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() { @Override public String then(Task task) throws Exception { if (acl.hasUnresolvedUser()) { throw new IllegalStateException("ACL has an unresolved EasimartUser. " + "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>() { @Override public Task then(Task 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 Task> fetchAllIfNeededInBackground( final List 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 EasimartException * Throws an exception if the server returns an error or is inaccessible. */ public static List fetchAllIfNeeded(List objects) throws EasimartException { return EasimartTaskUtils.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 void fetchAllIfNeededInBackground(final List objects, FindCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(fetchAllIfNeededInBackground(objects), callback); } private static Task> fetchAllAsync( final List objects, final boolean onlyIfNeeded) { return EasimartUser.getCurrentUserAsync().onSuccessTask(new Continuation>>() { @Override public Task> then(Task task) throws Exception { final EasimartUser user = task.getResult(); return enqueueForAll(objects, new Continuation>>() { @Override public Task> then(Task 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 EasimartUser.State private static Task> fetchAllAsync( final List objects, final EasimartUser user, final boolean onlyIfNeeded, Task toAwait) { if (objects.size() == 0) { return Task.forResult(objects); } List 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 EasimartQuery query = EasimartQuery.getQuery(className) .whereContainedIn(KEY_OBJECT_ID, objectIds); return toAwait.continueWithTask(new Continuation>>() { @Override public Task> then(Task task) throws Exception { return query.findAsync(query.getBuilder().build(), user, null); } }).onSuccess(new Continuation, List>() { @Override public List then(Task> task) throws Exception { Map 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 EasimartException( EasimartException.OBJECT_NOT_FOUND, "Object id " + object.getObjectId() + " does not exist"); } if (!Easimart.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 Task> fetchAllInBackground(final List 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 EasimartException * Throws an exception if the server returns an error or is inaccessible. */ public static List fetchAll(List objects) throws EasimartException { return EasimartTaskUtils.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 void fetchAllInBackground(List objects, FindCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(fetchAllInBackground(objects), callback); } /** * Return the operations that will be sent in the next call to save. */ private EasimartOperationSet currentOperations() { synchronized (mutex) { return operationSetQueue.getLast(); } } /** * Updates the estimated values in the map based on the given set of EasimartFieldOperations. */ private void applyOperations(EasimartOperationSet operations, Map map) { for (String key : operations.keySet()) { EasimartFieldOperation 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 (EasimartOperationSet operations : operationSetQueue) { applyOperations(operations, estimatedData); } } } /** * performOperation() is like {@link #put(String, Object)} but instead of just taking a new value, * it takes a EasimartFieldOperation that modifies the value. */ /* package */ void performOperation(String key, EasimartFieldOperation 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); } EasimartFieldOperation oldOperation = currentOperations().get(key); EasimartFieldOperation newOperation = operation.mergeWithPrevious(oldOperation); currentOperations().put(key, newOperation); } } /** * Add a key-value pair to this object. It is recommended to name keys in * camelCaseLikeThis. * * @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 EasimartObject}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 (!EasimartEncoder.isValidType(value)) { throw new IllegalArgumentException("invalid type for value: " + value.getClass().toString()); } performOperation(key, new EasimartSetOperation(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) { EasimartIncrementOperation operation = new EasimartIncrementOperation(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) { EasimartAddOperation operation = new EasimartAddOperation(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) { EasimartAddUniqueOperation operation = new EasimartAddUniqueOperation(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, EasimartDeleteOperation.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); EasimartRemoveOperation operation = new EasimartRemoveOperation(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}. */ //TODO (grantland): Do not auto-convert JSONArray and List (v2) // Calling this on an untouched fetched object will mark the object as dirty due to mutable // containers and auto List <-> JSONArray conversion, since arrays are converted to List // by default. public JSONArray getJSONArray(String key) { synchronized (mutex) { checkGetAccess(key); Object value = estimatedData.get(key); if (value instanceof List) { value = PointerOrLocalIdEncoder.get().encode(value); put(key, 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}. */ //TODO (grantland): Do not auto-convert JSONArray and List (v2) public List getList(String key) { synchronized (mutex) { Object value = estimatedData.get(key); if (value instanceof JSONArray) { EasimartDecoder decoder = EasimartDecoder.get(); value = decoder.convertJSONArrayToList((JSONArray) value); put(key, value); } if (!(value instanceof List)) { return null; } @SuppressWarnings("unchecked") List returnValue = (List) 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}. */ //TODO (grantland): Do not auto-convert JSONObject and Map (v2) public Map getMap(String key) { synchronized (mutex) { Object value = estimatedData.get(key); if (value instanceof JSONObject) { EasimartDecoder decoder = EasimartDecoder.get(); value = decoder.convertJSONObjectToMap((JSONObject) value); put(key, value); } if (!(value instanceof Map)) { return null; } @SuppressWarnings("unchecked") Map returnValue = (Map) 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}. */ //TODO (grantland): Do not auto-convert JSONObject and Map (v2) // Calling this on an untouched fetched object will mark the object as dirty due to mutable // containers and auto Map <-> JSONObject conversion, since maps are converted to Map // by default. public JSONObject getJSONObject(String key) { synchronized (mutex) { checkGetAccess(key); Object value = estimatedData.get(key); if (value instanceof Map) { value = PointerOrLocalIdEncoder.get().encode(value); put(key, 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 EasimartObject} value. This function will not perform a network request. Unless the * {@code EasimartObject} has been downloaded (e.g. by a {@link EasimartQuery#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 EasimartObject}. */ public EasimartObject getEasimartObject(String key) { Object value = get(key); if (!(value instanceof EasimartObject)) { return null; } return (EasimartObject) value; } /** * Access a {@link EasimartUser} value. This function will not perform a network request. Unless the * {@code EasimartObject} has been downloaded (e.g. by a {@link EasimartQuery#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 EasimartUser}. */ public EasimartUser getEasimartUser(String key) { Object value = get(key); if (!(value instanceof EasimartUser)) { return null; } return (EasimartUser) value; } /** * Access a {@link EasimartFile} value. This function will not perform a network request. Unless the * {@link EasimartFile} has been downloaded (e.g. by calling {@link EasimartFile#getData()}), * {@link EasimartFile#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 EasimartFile}. */ public EasimartFile getEasimartFile(String key) { Object value = get(key); if (!(value instanceof EasimartFile)) { return null; } return (EasimartFile) value; } /** * Access a {@link EasimartGeoPoint} 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 EasimartGeoPoint}. */ public EasimartGeoPoint getEasimartGeoPoint(String key) { synchronized (mutex) { checkGetAccess(key); Object value = estimatedData.get(key); if (!(value instanceof EasimartGeoPoint)) { return null; } return (EasimartGeoPoint) value; } } /** * Access the {@link EasimartACL} governing this object. */ public EasimartACL getACL() { return getACL(true); } private EasimartACL getACL(boolean mayCopy) { synchronized (mutex) { checkGetAccess(KEY_ACL); Object acl = estimatedData.get(KEY_ACL); if (acl == null) { return null; } if (!(acl instanceof EasimartACL)) { throw new RuntimeException("only ACLs can be stored in the ACL key"); } if (mayCopy && ((EasimartACL) acl).isShared()) { EasimartACL copy = ((EasimartACL) acl).copy(); estimatedData.put(KEY_ACL, copy); return copy; } return (EasimartACL) acl; } } /** * Set the {@link EasimartACL} governing this object. */ public void setACL(EasimartACL acl) { put(KEY_ACL, acl); } /** * Gets whether the {@code EasimartObject} has been fetched. * * @return {@code true} if the {@code EasimartObject} is new or has been fetched or refreshed. {@code false} * otherwise. */ public boolean isDataAvailable() { synchronized (mutex) { return state.isComplete(); } } /* package for tests */ boolean isDataAvailable(String key) { synchronized (mutex) { return isDataAvailable() || estimatedData.containsKey(key); } } /** * Access or create a {@link EasimartRelation} value for a key * * @param key * The key to access the relation for. * @return the EasimartRelation object if the relation already exists for the key or can be created * for this key. */ public EasimartRelation 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 EasimartRelation) { @SuppressWarnings("unchecked") EasimartRelation relation = (EasimartRelation) value; relation.ensureParentAndKey(this, key); return relation; } else { EasimartRelation relation = new EasimartRelation<>(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 * EasimartRelation 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 EasimartRelation) { ((EasimartRelation) value).ensureParentAndKey(this, key); } return value; } } private void checkGetAccess(String key) { if (!isDataAvailable(key)) { throw new IllegalStateException( "EasimartObject has no data for '" + key + "'. Call fetchIfNeeded() to get the data."); } } public boolean hasSameId(EasimartObject other) { synchronized (mutex) { return this.getClassName() != null && this.getObjectId() != null && this.getClassName().equals(other.getClassName()) && this.getObjectId().equals(other.getObjectId()); } } /* package */ void registerSaveListener(GetCallback callback) { synchronized (mutex) { saveEvent.subscribe(callback); } } /* package */ void unregisterSaveListener(GetCallback callback) { synchronized (mutex) { saveEvent.unsubscribe(callback); } } /** * Gets the class name based on the {@link EasimartClassName} annotation associated with a class. * * @param clazz * The class to inspect. * @return The name of the Easimart class, if one is provided. Otherwise, {@code null}. */ static String getClassName(Class clazz) { String name = classNames.get(clazz); if (name == null) { EasimartClassName info = clazz.getAnnotation(EasimartClassName.class); if (info == null) { return null; } name = info.value(); classNames.put(clazz, name); } return name; } /** * Called when a non-pointer is being created to allow additional initialization to occur. */ void setDefaultValues() { if (needsDefaultACL() && EasimartACL.getDefaultACL() != null) { this.setACL(EasimartACL.getDefaultACL()); } } /** * Determines whether this object should get a default ACL. Override in subclasses to turn off * default ACLs. */ boolean needsDefaultACL() { return true; } /** * Registers the Easimart-provided {@code EasimartObject} 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 registerEasimartSubclasses() { registerSubclass(EasimartUser.class); registerSubclass(EasimartRole.class); registerSubclass(EasimartInstallation.class); registerSubclass(EasimartSession.class); registerSubclass(EasimartPin.class); registerSubclass(EventuallyPin.class); } /* package */ static void unregisterEasimartSubclasses() { unregisterSubclass(EasimartUser.class); unregisterSubclass(EasimartRole.class); unregisterSubclass(EasimartInstallation.class); unregisterSubclass(EasimartSession.class); unregisterSubclass(EasimartPin.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 Easimart, 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 EasimartQuery#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 void pinAllInBackground(String name, List objects, SaveCallback callback) { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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 Task pinAllInBackground(final String name, final List objects) { return pinAllInBackground(name, objects, true); } private static Task pinAllInBackground(final String name, final List objects, final boolean includeAllChildren) { if (!Easimart.isLocalDatastoreEnabled()) { throw new IllegalStateException("Method requires Local Datastore. " + "Please refer to `Easimart#enableLocalDatastore(Context)`."); } Task task = Task.forResult(null); // Resolve and persist unresolved users attached via ACL, similarly how we do in saveAsync for (final EasimartObject object : objects) { task = task.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (!object.isDataAvailable(KEY_ACL)) { return Task.forResult(null); } final EasimartACL acl = object.getACL(false); if (acl == null) { return Task.forResult(null); } EasimartUser user = acl.getUnresolvedUser(); if (user == null || !user.isCurrentUser()) { return Task.forResult(null); } return EasimartUser.pinCurrentUserIfNeededAsync(user); } }); } return task.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return Easimart.getLocalDatastore().pinAllObjectsAsync( name != null ? name : DEFAULT_PIN, objects, includeAllChildren); } }).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { // Hack to emulate persisting current user on disk after a save like in EasimartUser#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 (EasimartCorePlugins.PIN_CURRENT_USER.equals(name)) { return task; } for (EasimartObject object : objects) { if (object instanceof EasimartUser) { final EasimartUser user = (EasimartUser) object; if (user.isCurrentUser()) { return EasimartUser.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 Easimart, 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 EasimartQuery#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 EasimartException */ public static void pinAll(String name, List objects) throws EasimartException { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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 void pinAllInBackground(List objects, SaveCallback callback) { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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 Task pinAllInBackground(List 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 Easimart, 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 EasimartQuery#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 EasimartException */ public static void pinAll(List objects) throws EasimartException { EasimartTaskUtils.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 void unpinAllInBackground(String name, List objects, DeleteCallback callback) { EasimartTaskUtils.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 Task unpinAllInBackground(String name, List objects) { if (!Easimart.isLocalDatastoreEnabled()) { throw new IllegalStateException("Method requires Local Datastore. " + "Please refer to `Easimart#enableLocalDatastore(Context)`."); } if (name == null) { name = DEFAULT_PIN; } return Easimart.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 EasimartException */ public static void unpinAll(String name, List objects) throws EasimartException { EasimartTaskUtils.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 void unpinAllInBackground(List objects, DeleteCallback callback) { EasimartTaskUtils.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 Task unpinAllInBackground(List 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 EasimartException */ public static void unpinAll(List objects) throws EasimartException { EasimartTaskUtils.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) { EasimartTaskUtils.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 unpinAllInBackground(String name) { if (!Easimart.isLocalDatastoreEnabled()) { throw new IllegalStateException("Method requires Local Datastore. " + "Please refer to `Easimart#enableLocalDatastore(Context)`."); } if (name == null) { name = DEFAULT_PIN; } return Easimart.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 EasimartException */ public static void unpinAll(String name) throws EasimartException { EasimartTaskUtils.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) { EasimartTaskUtils.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 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 EasimartException */ public static void unpinAll() throws EasimartException { EasimartTaskUtils.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 */ Task fetchFromLocalDatastoreAsync() { if (!Easimart.isLocalDatastoreEnabled()) { throw new IllegalStateException("Method requires Local Datastore. " + "Please refer to `Easimart#enableLocalDatastore(Context)`."); } return Easimart.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 void fetchFromLocalDatastoreInBackground(GetCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(this.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 EasimartException */ public void fetchFromLocalDatastore() throws EasimartException { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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) { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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 pinInBackground(String name) { return pinAllInBackground(name, Collections.singletonList(this)); } /* package */ Task 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 Easimart, 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 EasimartQuery#fromLocalDatastore()}, or you can create an unfetched pointer with * {@link #createWithoutData(Class, String)} and then call {@link #fetchFromLocalDatastore()} on * it. * * @see #unpin(String) * * @throws EasimartException */ public void pin(String name) throws EasimartException { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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) { EasimartTaskUtils.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 Easimart, 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 EasimartQuery#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 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 Easimart, 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 EasimartQuery#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 EasimartException */ public void pin() throws EasimartException { EasimartTaskUtils.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) { EasimartTaskUtils.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 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 EasimartException { EasimartTaskUtils.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) { EasimartTaskUtils.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 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 EasimartException { EasimartTaskUtils.wait(unpinInBackground()); } } // [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.




© 2015 - 2024 Weber Informatics LLC | Privacy Policy