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

co.easimart.EasimartUser Maven / Gradle / Ivy

package co.easimart;

import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import bolts.Continuation;
import bolts.Task;

/**
 * The {@code EasimartUser} is a local representation of user data that can be saved and retrieved from
 * the Easimart cloud.
 */
@EasimartClassName("_User")
public class EasimartUser extends EasimartObject {

  private static final String KEY_SESSION_TOKEN = "sessionToken";
  private static final String KEY_AUTH_DATA = "authData";
  private static final String KEY_USERNAME = "username";
  private static final String KEY_PASSWORD = "password";
  private static final String KEY_EMAIL = "email";

  private static final List READ_ONLY_KEYS = Collections.unmodifiableList(
      Arrays.asList(KEY_SESSION_TOKEN, KEY_AUTH_DATA));

  /**
   * Constructs a query for {@code EasimartUser}.
   *
   * @see EasimartQuery#getQuery(Class)
   */
  public static EasimartQuery getQuery() {
    return EasimartQuery.getQuery(EasimartUser.class);
  }

  /* package for tests */ static EasimartUserController getUserController() {
    return EasimartCorePlugins.getInstance().getUserController();
  }

  /* package for tests */ static EasimartCurrentUserController getCurrentUserController() {
    return EasimartCorePlugins.getInstance().getCurrentUserController();
  }

  /* package for tests */ static EasimartAuthenticationManager getAuthenticationManager() {
    return EasimartCorePlugins.getInstance().getAuthenticationManager();
  }

  /** package */ static class State extends EasimartObject.State {

    /** package */ static class Builder extends Init {

      private boolean isNew;

      public Builder() {
        super("_User");
      }

      /* package */ Builder(State state) {
        super(state);
        isNew = state.isNew();
      }

      @Override
      /* package */ Builder self() {
        return this;
      }

      @SuppressWarnings("unchecked")
      @Override
      public State build() {
        return new State(this);
      }

      @Override
      public Builder apply(EasimartObject.State other) {
        isNew(((State) other).isNew());
        return super.apply(other);
      }

      public Builder sessionToken(String sessionToken) {
        return put(KEY_SESSION_TOKEN, sessionToken);
      }

      public Builder authData(Map> authData) {
        return put(KEY_AUTH_DATA, authData);
      }

      @SuppressWarnings("unchecked")
      public Builder putAuthData(String authType, Map authData) {
        Map> newAuthData =
            (Map>) serverData.get(KEY_AUTH_DATA);
        if (newAuthData == null) {
          newAuthData = new HashMap<>();
        }
        newAuthData.put(authType, authData);
        serverData.put(KEY_AUTH_DATA, newAuthData);
        return this;
      }

      public Builder isNew(boolean isNew) {
        this.isNew = isNew;
        return this;
      }
    }

    private final boolean isNew;

    private State(Builder builder) {
      super(builder);
      isNew = builder.isNew;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Builder newBuilder() {
      return new Builder(this);
    }

    public String sessionToken() {
      return (String) get(KEY_SESSION_TOKEN);
    }

    @SuppressWarnings("unchecked")
    public Map> authData() {
      Map> authData =
          (Map>) get(KEY_AUTH_DATA);
      if (authData == null) {
        // We'll always return non-null for now since we don't have any null checking in place.
        // Be aware not to get and set without checking size or else we'll be adding a value that
        // wasn't there in the first place.
        authData = new HashMap<>();
      }
      return authData;
    }

    public boolean isNew() {
      return isNew;
    }
  }

  // Whether the object is a currentUser. If so, it will always be persisted to disk on updates.
  private boolean isCurrentUser;

  /**
   * Constructs a new EasimartUser with no data in it. A EasimartUser constructed in this way will not
   * have an objectId and will not persist to the database until {@link #signUp} is called.
   */
  public EasimartUser() {
    isCurrentUser = false;
  }

  @Override
  /* package */ boolean needsDefaultACL() {
    return false;
  }

  @Override
  boolean isKeyMutable(String key) {
    return !READ_ONLY_KEYS.contains(key);
  }

  @Override
  /* package */ State.Builder newStateBuilder(String className) {
    return new State.Builder();
  }

  @Override
  /* package */ State getState() {
    return (State) super.getState();
  }

  /**
   * @return {@code true} if this user was created with {@link #getCurrentUser()} when no current
   * user previously existed and {@link #enableAutomaticUser()} is set, false if was created by any
   * other means or if a previously "lazy" user was saved remotely.
   */
  /* package */ boolean isLazy() {
    synchronized (mutex) {
      return getObjectId() == null && EasimartAnonymousUtils.isLinked(this);
    }
  }

  /**
   * Whether the EasimartUser has been authenticated on this device. This will be true if the EasimartUser
   * was obtained via a logIn or signUp method. Only an authenticated EasimartUser can be saved (with
   * altered attributes) and deleted.
   */
  public boolean isAuthenticated() {
    synchronized (mutex) {
      EasimartUser current = EasimartUser.getCurrentUser();
      return isLazy() ||
          (getState().sessionToken() != null
              && current != null
              && getObjectId().equals(current.getObjectId()));
    }
  }

  @Override
  public void remove(String key) {
    if (KEY_USERNAME.equals(key)) {
      throw new IllegalArgumentException("Can't remove the username key.");
    }
    super.remove(key);
  }

  @Override
  /* package */ JSONObject toRest(
      EasimartObject.State state,
      List operationSetQueue,
      EasimartEncoder objectEncoder) {
    // Create a sanitized copy of operationSetQueue with `password` removed if necessary
    List cleanOperationSetQueue = operationSetQueue;
    for (int i = 0; i < operationSetQueue.size(); i++) {
      EasimartOperationSet operations = operationSetQueue.get(i);
      if (operations.containsKey(KEY_PASSWORD)) {
        if (cleanOperationSetQueue == operationSetQueue) {
          cleanOperationSetQueue = new LinkedList<>(operationSetQueue);
        }
        EasimartOperationSet cleanOperations = new EasimartOperationSet(operations);
        cleanOperations.remove(KEY_PASSWORD);
        cleanOperationSetQueue.set(i, cleanOperations);
      }
    }
    return super.toRest(state, cleanOperationSetQueue, objectEncoder);
  }

  /* package for tests */ Task cleanUpAuthDataAsync() {
    EasimartAuthenticationManager controller = getAuthenticationManager();
    Map> authData;
    synchronized (mutex) {
      authData = getState().authData();
      if (authData.size() == 0) {
        return Task.forResult(null); // Nothing to see or do here...
      }
    }

    List> tasks = new ArrayList<>();
    Iterator>> i = authData.entrySet().iterator();
    while (i.hasNext()) {
      Map.Entry> entry = i.next();
      if (entry.getValue() == null) {
        i.remove();
        tasks.add(controller.restoreAuthenticationAsync(entry.getKey(), null).makeVoid());
      }
    }

    State newState = getState().newBuilder()
        .authData(authData)
        .build();
    setState(newState);

    return Task.whenAll(tasks);
  }

  @Override
  /* package */ Task handleSaveResultAsync(
      EasimartObject.State result, EasimartOperationSet operationsBeforeSave) {
    boolean success = result != null;
    if (success) {
      operationsBeforeSave.remove(KEY_PASSWORD);
    }

    return super.handleSaveResultAsync(result, operationsBeforeSave);
  }

  @Override
  /* package */ void validateSaveEventually() throws EasimartException {
    if (isDirty(KEY_PASSWORD)) {
      // TODO(mengyan): Unify the exception we throw when validate fails
      throw new EasimartException(
          EasimartException.OTHER_CAUSE,
          "Unable to saveEventually on a EasimartUser with dirty password");
    }
  }

  //region Getter/Setter helper methods

  /* package */ boolean isCurrentUser() {
    synchronized (mutex) {
      return isCurrentUser;
    }
  }

  /* package */ void setIsCurrentUser(boolean isCurrentUser) {
    synchronized (mutex) {
      this.isCurrentUser = isCurrentUser;
    }
  }

  /**
   * @return the session token for a user, if they are logged in.
   */
  public String getSessionToken() {
    return getState().sessionToken();
  }

  // This is only used when upgrading to revocable session
  private Task setSessionTokenInBackground(String newSessionToken) {
    synchronized (mutex) {
      State state = getState();
      if (newSessionToken.equals(state.sessionToken())) {
        return Task.forResult(null);
      }

      State.Builder builder = state.newBuilder()
          .sessionToken(newSessionToken);
      setState(builder.build());
      return saveCurrentUserAsync(this);
    }
  }

  /* package for testes */ Map> getAuthData() {
    synchronized (mutex) {
      Map> authData = getMap(KEY_AUTH_DATA);
      if (authData == null) {
        // We'll always return non-null for now since we don't have any null checking in place.
        // Be aware not to get and set without checking size or else we'll be adding a value that
        // wasn't there in the first place.
        authData = new HashMap<>();
      }
      return authData;
    }
  }

  private Map getAuthData(String authType) {
    return getAuthData().get(authType);
  }

  /* package */ void putAuthData(String authType, Map authData) {
    synchronized (mutex) {
      Map> newAuthData = getAuthData();
      newAuthData.put(authType, authData);
      performPut(KEY_AUTH_DATA, newAuthData);
    }
  }

  private void removeAuthData(String authType) {
    synchronized (mutex) {
      Map> newAuthData = getAuthData();
      newAuthData.remove(authType);
      performPut(KEY_AUTH_DATA, newAuthData);
    }
  }

  /**
   * Sets the username. Usernames cannot be null or blank.
   *
   * @param username
   *          The username to set.
   */
  public void setUsername(String username) {
    put(KEY_USERNAME, username);
  }

  /**
   * Retrieves the username.
   */
  public String getUsername() {
    return getString(KEY_USERNAME);
  }

  /**
   * Sets the password.
   *
   * @param password
   *          The password to set.
   */
  public void setPassword(String password) {
    put(KEY_PASSWORD, password);
  }

  /* package for tests */ String getPassword() {
    return getString(KEY_PASSWORD);
  }

  /**
   * Sets the email address.
   *
   * @param email
   *          The email address to set.
   */
  public void setEmail(String email) {
    put(KEY_EMAIL, email);
  }

  /**
   * Retrieves the email address.
   */
  public String getEmail() {
    return getString(KEY_EMAIL);
  }

  /**
   * Indicates whether this {@code EasimartUser} was created during this session through a call to
   * {@link #signUp()} or by logging in with a linked service such as Facebook.
   */
  public boolean isNew() {
    return getState().isNew();
  }

  //endregion

  @Override
  public void put(String key, Object value) {
    synchronized (mutex) {
      if (KEY_USERNAME.equals(key)) {
        // When the username is set, remove any semblance of anonymity.
        stripAnonymity();
      }
      super.put(key, value);
    }
  }

  private void stripAnonymity() {
    synchronized (mutex) {
      if (EasimartAnonymousUtils.isLinked(this)) {
        if (getObjectId() != null) {
          putAuthData(EasimartAnonymousUtils.AUTH_TYPE, null);
        } else {
          removeAuthData(EasimartAnonymousUtils.AUTH_TYPE);
        }
      }
    }
  }

  // TODO(grantland): Can we replace this with #revert(String)?
  private void restoreAnonymity(Map anonymousData) {
    synchronized (mutex) {
      if (anonymousData != null) {
        putAuthData(EasimartAnonymousUtils.AUTH_TYPE, anonymousData);
      }
    }
  }

  @Override
  /* package */ void validateSave() {
    synchronized (mutex) {
      if (getObjectId() == null) {
        throw new IllegalArgumentException(
            "Cannot save a EasimartUser until it has been signed up. Call signUp first.");
      }

      if (isAuthenticated() || !isDirty() || isCurrentUser()) {
        return;
      }
    }

    if (!Easimart.isLocalDatastoreEnabled()) {
      // This might be a different of instance of the currentUser, so we need to check objectIds
      EasimartUser current = EasimartUser.getCurrentUser(); //TODO (grantland): possible blocking disk i/o
      if (current != null && getObjectId().equals(current.getObjectId())) {
        return;
      }
    }

    throw new IllegalArgumentException("Cannot save a EasimartUser that is not authenticated.");
  }

  @Override
  /* package */ Task saveAsync(String sessionToken, Task toAwait) {
    return saveAsync(sessionToken, isLazy(), toAwait);
  }

  /* package for tests */ Task saveAsync(String sessionToken, boolean isLazy, Task toAwait) {
    Task task;
    if (isLazy) {
      task = resolveLazinessAsync(toAwait);
    } else {
      task = super.saveAsync(sessionToken, toAwait);
    }

    if (isCurrentUser()) {
      // If the user is the currently logged in user, we persist all data to disk
      return task.onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
            return cleanUpAuthDataAsync();
        }
      }).onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
          return saveCurrentUserAsync(EasimartUser.this);
        }
      });
    }

    return task;
  }

  @Override
  /* package */ void setState(EasimartObject.State newState) {
    // Avoid clearing sessionToken when updating the current user's State via EasimartQuery result
    if (isCurrentUser() && getSessionToken() != null
            && newState.get("sessionToken") == null) {
      newState = newState.newBuilder().put("sessionToken", getSessionToken()).build();
    }
    super.setState(newState);
  }

  @Override
  /* package */ void validateDelete() {
    synchronized (mutex) {
      super.validateDelete();
      if (!isAuthenticated() && isDirty()) {
        throw new IllegalArgumentException("Cannot delete a EasimartUser that is not authenticated.");
      }
    }
  }

  @SuppressWarnings("unchecked")
  @Override
  public EasimartUser fetch() throws EasimartException {
    return (EasimartUser) super.fetch();
  }

  @SuppressWarnings("unchecked")
  @Override
  /* package */  Task fetchAsync(
      String sessionToken, Task toAwait) {
    //TODO (grantland): It doesn't seem like we should do this.. Why don't we error like we do
    // when fetching an unsaved EasimartObject?
    if (isLazy()) {
      return Task.forResult((T) this);
    }

    Task task = super.fetchAsync(sessionToken, toAwait);

    if (isCurrentUser()) {
      return task.onSuccessTask(new Continuation>() {
        @Override
        public Task then(final Task fetchAsyncTask) throws Exception {
          return cleanUpAuthDataAsync();
        }
      }).onSuccessTask(new Continuation>() {
        @Override
        public Task then(Task task) throws Exception {
          return saveCurrentUserAsync(EasimartUser.this);
        }
      }).onSuccess(new Continuation() {
        @Override
        public T then(Task task) throws Exception {
          return (T) EasimartUser.this;
        }
      });
    }

    return task;
  }

  /**
   * Signs up a new user. You should call this instead of {@link #save} for new EasimartUsers. This
   * will create a new EasimartUser on the server, and also persist the session on disk so that you can
   * access the user using {@link #getCurrentUser}.
   * 

* A username and password must be set before calling signUp. *

* This is preferable to using {@link #signUp}, unless your code is already running from a * background thread. * * @return A Task that is resolved when sign up completes. */ public Task signUpInBackground() { return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task task) throws Exception { return signUpAsync(task); } }); } /* package for tests */ Task signUpAsync(Task toAwait) { final EasimartUser user = getCurrentUser(); //TODO (grantland): convert to async synchronized (mutex) { final String sessionToken = user != null ? user.getSessionToken() : null; if (EasimartTextUtils.isEmpty(getUsername())) { return Task.forError(new IllegalArgumentException("Username cannot be missing or blank")); } if (EasimartTextUtils.isEmpty(getPassword())) { return Task.forError(new IllegalArgumentException("Password cannot be missing or blank")); } if (getObjectId() != null) { // For anonymous users, there may be an objectId. Setting the // userName will have removed the anonymous link and set the // value in the authData object to JSONObject.NULL, so we can // just treat it like a save operation. Map> authData = getAuthData(); if (authData.containsKey(EasimartAnonymousUtils.AUTH_TYPE) && authData.get(EasimartAnonymousUtils.AUTH_TYPE) == null) { return saveAsync(sessionToken, toAwait); } // Otherwise, throw. return Task.forError( new IllegalArgumentException("Cannot sign up a user that has already signed up.")); } // If the operationSetQueue is has operation sets in it, then a save or signUp is in progress. // If there is a signUp or save already in progress, don't allow another one to start. if (operationSetQueue.size() > 1) { return Task.forError( new IllegalArgumentException("Cannot sign up a user that is already signing up.")); } // If the current user is an anonymous user, merge this object's data into the anonymous user // and save. if (user != null && EasimartAnonymousUtils.isLinked(user)) { // this doesn't have any outstanding saves, so we can safely merge its operations into the // current user. if (this == user) { return Task.forError( new IllegalArgumentException("Attempt to merge currentUser with itself.")); } boolean isLazy = user.isLazy(); final String oldUsername = user.getUsername(); final String oldPassword = user.getPassword(); final Map anonymousData = user.getAuthData(EasimartAnonymousUtils.AUTH_TYPE); user.copyChangesFrom(this); user.setUsername(getUsername()); user.setPassword(getPassword()); revert(); return user.saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (task.isCancelled() || task.isFaulted()) { // Error synchronized (user.mutex) { if (oldUsername != null) { user.setUsername(oldUsername); } else { user.revert(KEY_USERNAME); } if (oldPassword != null) { user.setPassword(oldPassword); } else { user.revert(KEY_PASSWORD); } user.restoreAnonymity(anonymousData); } return task; } else { // Success user.revert(KEY_PASSWORD); revert(KEY_PASSWORD); } mergeFromObject(user); return saveCurrentUserAsync(EasimartUser.this); } }); } final EasimartOperationSet operations = startSave(); return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return getUserController().signUpAsync( getState(), operations, sessionToken ).continueWithTask(new Continuation>() { @Override public Task then(final Task signUpTask) throws Exception { EasimartUser.State result = signUpTask.getResult(); return handleSaveResultAsync(result, operations).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (!signUpTask.isCancelled() && !signUpTask.isFaulted()) { return saveCurrentUserAsync(EasimartUser.this); } return signUpTask.makeVoid(); } }); } }); } }); } } /** * Signs up a new user. You should call this instead of {@link #save} for new EasimartUsers. This * will create a new EasimartUser on the server, and also persist the session on disk so that you can * access the user using {@link #getCurrentUser}. *

* A username and password must be set before calling signUp. *

* Typically, you should use {@link #signUpInBackground} instead of this, unless you are managing * your own threading. * * @throws EasimartException * Throws an exception if the server is inaccessible, or if the username has already * been taken. */ public void signUp() throws EasimartException { EasimartTaskUtils.wait(signUpInBackground()); } /** * Signs up a new user. You should call this instead of {@link #save} for new EasimartUsers. This * will create a new EasimartUser on the server, and also persist the session on disk so that you can * access the user using {@link #getCurrentUser}. *

* A username and password must be set before calling signUp. *

* This is preferable to using {@link #signUp}, unless your code is already running from a * background thread. * * @param callback * callback.done(user, e) is called when the signUp completes. */ public void signUpInBackground(SignUpCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(signUpInBackground(), callback); } /** * Logs in a user with a username and password. On success, this saves the session to disk, so you * can retrieve the currently logged in user using {@link #getCurrentUser}. *

* This is preferable to using {@link #logIn}, unless your code is already running from a * background thread. * * @param username * The username to log in with. * @param password * The password to log in with. * * @return A Task that is resolved when logging in completes. */ public static Task logInInBackground(String username, String password) { if (username == null) { throw new IllegalArgumentException("Must specify a username for the user to log in with"); } if (password == null) { throw new IllegalArgumentException("Must specify a password for the user to log in with"); } return getUserController().logInAsync(username, password).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { State result = task.getResult(); final EasimartUser newCurrent = EasimartObject.from(result); return saveCurrentUserAsync(newCurrent).onSuccess(new Continuation() { @Override public EasimartUser then(Task task) throws Exception { return newCurrent; } }); } }); } /** * Logs in a user with a username and password. On success, this saves the session to disk, so you * can retrieve the currently logged in user using {@link #getCurrentUser}. *

* Typically, you should use {@link #logInInBackground} instead of this, unless you are managing * your own threading. * * @param username * The username to log in with. * @param password * The password to log in with. * @throws EasimartException * Throws an exception if the login was unsuccessful. * @return The user if the login was successful. */ public static EasimartUser logIn(String username, String password) throws EasimartException { return EasimartTaskUtils.wait(logInInBackground(username, password)); } /** * Logs in a user with a username and password. On success, this saves the session to disk, so you * can retrieve the currently logged in user using {@link #getCurrentUser}. *

* This is preferable to using {@link #logIn}, unless your code is already running from a * background thread. * * @param username * The username to log in with. * @param password * The password to log in with. * @param callback * callback.done(user, e) is called when the login completes. */ public static void logInInBackground(final String username, final String password, LogInCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(logInInBackground(username, password), callback); } /** * Authorize a user with a session token. On success, this saves the session to disk, so you can * retrieve the currently logged in user using {@link #getCurrentUser}. *

* This is preferable to using {@link #become}, unless your code is already running from a * background thread. * * @param sessionToken * The session token to authorize with. * * @return A Task that is resolved when authorization completes. */ public static Task becomeInBackground(String sessionToken) { if (sessionToken == null) { throw new IllegalArgumentException("Must specify a sessionToken for the user to log in with"); } return getUserController().getUserAsync(sessionToken).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { State result = task.getResult(); final EasimartUser user = EasimartObject.from(result); return saveCurrentUserAsync(user).onSuccess(new Continuation() { @Override public EasimartUser then(Task task) throws Exception { return user; } }); } }); } /** * Authorize a user with a session token. On success, this saves the session to disk, so you can * retrieve the currently logged in user using {@link #getCurrentUser}. *

* Typically, you should use {@link #becomeInBackground} instead of this, unless you are managing * your own threading. * * @param sessionToken * The session token to authorize with. * @throws EasimartException * Throws an exception if the authorization was unsuccessful. * @return The user if the authorization was successful. */ public static EasimartUser become(String sessionToken) throws EasimartException { return EasimartTaskUtils.wait(becomeInBackground(sessionToken)); } /** * Authorize a user with a session token. On success, this saves the session to disk, so you can * retrieve the currently logged in user using {@link #getCurrentUser}. *

* This is preferable to using {@link #become}, unless your code is already running from a * background thread. * * @param sessionToken * The session token to authorize with. * @param callback * callback.done(user, e) is called when the authorization completes. */ public static void becomeInBackground(final String sessionToken, LogInCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(becomeInBackground(sessionToken), callback); } //TODO (grantland): Publicize /* package */ static Task getCurrentUserAsync() { return getCurrentUserController().getAsync(); } /** * This retrieves the currently logged in EasimartUser with a valid session, either from memory or * disk if necessary. * * @return The currently logged in EasimartUser */ public static EasimartUser getCurrentUser() { return getCurrentUser(isAutomaticUserEnabled()); } /** * This retrieves the currently logged in EasimartUser with a valid session, either from memory or * disk if necessary. * * @param shouldAutoCreateUser * {@code true} to automatically create and set an anonymous user as current. * @return The currently logged in EasimartUser */ private static EasimartUser getCurrentUser(boolean shouldAutoCreateUser) { try { return EasimartTaskUtils.wait(getCurrentUserController().getAsync(shouldAutoCreateUser)); } catch (EasimartException e) { //TODO (grantland): Publicize this exception return null; } } //TODO (grantland): Make it throw EasimartException and call #getCurrenSessionTokenInBackground() /* package */ static String getCurrentSessionToken() { EasimartUser current = EasimartUser.getCurrentUser(); return current != null ? current.getSessionToken() : null; } //TODO (grantland): Make it really async and publicize in v2 /* package */ static Task getCurrentSessionTokenAsync() { return getCurrentUserController().getCurrentSessionTokenAsync(); } // Persists a user as currentUser to disk, and into the singleton private static Task saveCurrentUserAsync(EasimartUser user) { return getCurrentUserController().setAsync(user); } /** * Used by {@link EasimartObject#pin} to persist lazy users to LDS that haven't been pinned yet. */ /* package */ static Task pinCurrentUserIfNeededAsync(EasimartUser user) { if (!Easimart.isLocalDatastoreEnabled()) { throw new IllegalStateException("Method requires Local Datastore. " + "Please refer to `Easimart#enableLocalDatastore(Context)`."); } return getCurrentUserController().setIfNeededAsync(user); } /** * Logs out the currently logged in user session. This will remove the session from disk, log out * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. *

* This is preferable to using {@link #logOut}, unless your code is already running from a * background thread. * * @return A Task that is resolved when logging out completes. */ public static Task logOutInBackground() { return getCurrentUserController().logOutAsync(); } /** * Logs out the currently logged in user session. This will remove the session from disk, log out * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. *

* This is preferable to using {@link #logOut}, unless your code is already running from a * background thread. */ public static void logOutInBackground(LogOutCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(logOutInBackground(), callback); } /** * Logs out the currently logged in user session. This will remove the session from disk, log out * of linked services, and future calls to {@link #getCurrentUser()} will return {@code null}. *

* Typically, you should use {@link #logOutInBackground()} instead of this, unless you are * managing your own threading. *

* Note:: Any errors in the log out flow will be swallowed due to * backward-compatibility reasons. Please use {@link #logOutInBackground()} if you'd wish to * handle them. */ public static void logOut() { try { EasimartTaskUtils.wait(logOutInBackground()); } catch (EasimartException e) { //TODO (grantland): We shouldn't swallow errors, but we need to for backwards compatibility. // Change this in v2. } } //TODO (grantland): Add to taskQueue /* package */ Task logOutAsync() { return logOutAsync(true); } /* package */ Task logOutAsync(boolean revoke) { EasimartAuthenticationManager controller = getAuthenticationManager(); List> tasks = new ArrayList<>(); final String oldSessionToken; synchronized (mutex) { oldSessionToken = getState().sessionToken(); for (Map.Entry> entry : getAuthData().entrySet()) { tasks.add(controller.deauthenticateAsync(entry.getKey())); } State newState = getState().newBuilder() .sessionToken(null) .isNew(false) .build(); isCurrentUser = false; setState(newState); } if (revoke) { tasks.add(EasimartSession.revokeAsync(oldSessionToken)); } return Task.whenAll(tasks); } /** * Requests a password reset email to be sent in a background thread to the specified email * address associated with the user account. This email allows the user to securely reset their * password on the Easimart site. *

* This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already * running from a background thread. * * @param email * The email address associated with the user that forgot their password. * * @return A Task that is resolved when the command completes. */ public static Task requestPasswordResetInBackground(String email) { return getUserController().requestPasswordResetAsync(email); } /** * Requests a password reset email to be sent to the specified email address associated with the * user account. This email allows the user to securely reset their password on the Easimart site. *

* Typically, you should use {@link #requestPasswordResetInBackground} instead of this, unless you * are managing your own threading. * * @param email * The email address associated with the user that forgot their password. * @throws EasimartException * Throws an exception if the server is inaccessible, or if an account with that email * doesn't exist. */ public static void requestPasswordReset(String email) throws EasimartException { EasimartTaskUtils.wait(requestPasswordResetInBackground(email)); } /** * Requests a password reset email to be sent in a background thread to the specified email * address associated with the user account. This email allows the user to securely reset their * password on the Easimart site. *

* This is preferable to using {@link #requestPasswordReset(String)}, unless your code is already * running from a background thread. * * @param email * The email address associated with the user that forgot their password. * @param callback * callback.done(e) is called when the request completes. */ public static void requestPasswordResetInBackground(final String email, RequestPasswordResetCallback callback) { EasimartTaskUtils.callbackOnMainThreadAsync(requestPasswordResetInBackground(email), callback); } @SuppressWarnings("unchecked") @Override public EasimartUser fetchIfNeeded() throws EasimartException { return super.fetchIfNeeded(); } //region Third party authentication /** * Registers a third party authentication callback. *

* Note: This shouldn't be called directly unless developing a third party authentication * library. * * @param authType The name of the third party authentication source. * @param callback The third party authentication callback to be registered. * * @see AuthenticationCallback */ public static void registerAuthenticationCallback( String authType, AuthenticationCallback callback) { getAuthenticationManager().register(authType, callback); } /** * Logs in a user with third party authentication credentials. *

* Note: This shouldn't be called directly unless developing a third party authentication * library. * * @param authType The name of the third party authentication source. * @param authData The user credentials of the third party authentication source. * @return A {@code Task} is resolved when logging in completes. * * @see AuthenticationCallback */ public static Task logInWithInBackground( final String authType, final Map authData) { if (authType == null) { throw new IllegalArgumentException("Invalid authType: " + null); } final Continuation> logInWithTask = new Continuation>() { @Override public Task then(Task task) throws Exception { return getUserController().logInAsync(authType, authData).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { EasimartUser.State result = task.getResult(); final EasimartUser user = EasimartObject.from(result); return saveCurrentUserAsync(user).onSuccess(new Continuation() { @Override public EasimartUser then(Task task) throws Exception { return user; } }); } }); } }; // Handle claiming of user. return getCurrentUserAsync().onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final EasimartUser user = task.getResult(); if (user != null) { synchronized (user.mutex) { if (EasimartAnonymousUtils.isLinked(user)) { if (user.isLazy()) { final Map oldAnonymousData = user.getAuthData(EasimartAnonymousUtils.AUTH_TYPE); return user.taskQueue.enqueue(new Continuation>() { @Override public Task then(final Task toAwait) throws Exception { return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { synchronized (user.mutex) { // Replace any anonymity with the new linked authData. user.stripAnonymity(); user.putAuthData(authType, authData); return user.resolveLazinessAsync(task); } } }).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { synchronized (user.mutex) { if (task.isFaulted()) { user.removeAuthData(authType); user.restoreAnonymity(oldAnonymousData); return Task.forError(task.getError()); } if (task.isCancelled()) { return Task.cancelled(); } return Task.forResult(user); } } }); } }); } else { // Try to link the current user with third party user, unless a user is already linked // to that third party user, then we'll just create a new user and link it with the // third party user. New users will not be linked to the previous user's data. return user.linkWithInBackground(authType, authData) .continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { if (task.isFaulted()) { Exception error = task.getError(); if (error instanceof EasimartException && ((EasimartException) error).getCode() == EasimartException.ACCOUNT_ALREADY_LINKED) { // An account that's linked to the given authData already exists, so log in // instead of trying to claim. return Task.forResult(null).continueWithTask(logInWithTask); } } if (task.isCancelled()) { return Task.cancelled(); } return Task.forResult(user); } }); } } } } return Task.forResult(null).continueWithTask(logInWithTask); } }); } /** * Indicates whether this user is linked with a third party authentication source. *

* Note: This shouldn't be called directly unless developing a third party authentication * library. * * @param authType The name of the third party authentication source. * @return {@code true} if linked, otherwise {@code false}. * * @see AuthenticationCallback */ public boolean isLinked(String authType) { Map> authData = getAuthData(); return authData.containsKey(authType) && authData.get(authType) != null; } /** * Ensures that all auth sources have auth data (e.g. access tokens, etc.) that matches this * user. */ /* package */ Task synchronizeAllAuthDataAsync() { Map> authData; synchronized (mutex) { if (!isCurrentUser()) { return Task.forResult(null); } authData = getAuthData(); } List> tasks = new ArrayList<>(authData.size()); for (String authType : authData.keySet()) { tasks.add(synchronizeAuthDataAsync(authType)); } return Task.whenAll(tasks); } /* package */ Task synchronizeAuthDataAsync(String authType) { Map authData; synchronized (mutex) { if (!isCurrentUser()) { return Task.forResult(null); } authData = getAuthData(authType); } return synchronizeAuthDataAsync(getAuthenticationManager(), authType, authData); } private Task synchronizeAuthDataAsync( EasimartAuthenticationManager manager, final String authType, Map authData) { return manager.restoreAuthenticationAsync(authType, authData).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { boolean success = !task.isFaulted() && task.getResult(); if (!success) { return unlinkFromInBackground(authType); } return task.makeVoid(); } }); } private Task linkWithAsync( final String authType, final Map authData, final Task toAwait, final String sessionToken) { synchronized (mutex) { final boolean isLazy = isLazy(); final Map oldAnonymousData = getAuthData(EasimartAnonymousUtils.AUTH_TYPE); stripAnonymity(); putAuthData(authType, authData); return saveAsync(sessionToken, isLazy, toAwait).continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { synchronized (mutex) { if (task.isFaulted() || task.isCancelled()) { restoreAnonymity(oldAnonymousData); return task; } return synchronizeAuthDataAsync(authType); } } }); } } private Task linkWithAsync( final String authType, final Map authData, final String sessionToken) { return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task task) throws Exception { return linkWithAsync(authType, authData, task, sessionToken); } }); } /** * Links this user to a third party authentication source. *

* Note: This shouldn't be called directly unless developing a third party authentication * library. * * @param authType The name of the third party authentication source. * @param authData The user credentials of the third party authentication source. * @return A {@code Task} is resolved when linking completes. * * @see AuthenticationCallback */ public Task linkWithInBackground( String authType, Map authData) { if (authType == null) { throw new IllegalArgumentException("Invalid authType: " + null); } return linkWithAsync(authType, authData, getSessionToken()); } /** * Unlinks this user from a third party authentication source. *

* Note: This shouldn't be called directly unless developing a third party authentication * library. * * @param authType The name of the third party authentication source. * @return A {@code Task} is resolved when unlinking completes. * * @see AuthenticationCallback */ public Task unlinkFromInBackground(final String authType) { if (authType == null) { return Task.forResult(null); } synchronized (mutex) { if (!getAuthData().containsKey(authType)) { return Task.forResult(null); } putAuthData(authType, null); } return saveInBackground(); } //endregion /** * Try to resolve a lazy user. * * If {@code authData} is empty, we'll treat this just as a SignUp. Otherwise, we'll * treat this as a SignUpOrLogIn. We'll merge the server result with this user, only if LDS is not * enabled. * * @param toAwait {@code Task} to wait for completion before running. * @return A {@code Task} that will resolve to the current user. If this is a SignUp it'll be this * {@code EasimartUser} instance, otherwise it'll be a new {@code EasimartUser} instance. */ /* package for tests */ Task resolveLazinessAsync(Task toAwait) { synchronized (mutex) { if (getAuthData().size() == 0) { // TODO(grantland): Could we just check isDirty(KEY_AUTH_DATA)? // If there are no linked services, treat this as a SignUp. return signUpAsync(toAwait); } final EasimartOperationSet operations = startSave(); // Otherwise, treat this as a SignUpOrLogIn return toAwait.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return getUserController().logInAsync(getState(), operations).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { final EasimartUser.State result = task.getResult(); Task resultTask; // We can't merge this user with the server if this is a LogIn because LDS might // already be keeping track of the servers objectId. if (Easimart.isLocalDatastoreEnabled() && !result.isNew()) { resultTask = Task.forResult(result); } else { resultTask = handleSaveResultAsync(result, operations).onSuccess(new Continuation() { @Override public EasimartUser.State then(Task task) throws Exception { return result; } }); } return resultTask.onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { EasimartUser.State result = task.getResult(); if (!result.isNew()) { // If the result is not a new user, treat this as a fresh logIn with complete // serverData, and switch the current user to the new user. final EasimartUser newUser = EasimartObject.from(result); return saveCurrentUserAsync(newUser); } return task.makeVoid(); } }); } }); } }); } } @SuppressWarnings("unchecked") @Override /* package */ Task fetchFromLocalDatastoreAsync() { // Same as #fetch() if (isLazy()) { return Task.forResult((T) this); } return super.fetchFromLocalDatastoreAsync(); } //region Automatic User private static final Object isAutoUserEnabledMutex = new Object(); private static boolean autoUserEnabled; /** * Enables automatic creation of anonymous users. After calling this method, * {@link #getCurrentUser()} will always have a value. The user will only be created on the server * once the user has been saved, or once an object with a relation to that user or an ACL that * refers to the user has been saved. *

* Note: {@link EasimartObject#saveEventually()} will not work if an item being * saved has a relation to an automatic user that has never been saved. */ public static void enableAutomaticUser() { synchronized (isAutoUserEnabledMutex) { autoUserEnabled = true; } } /* package */ static void disableAutomaticUser() { synchronized (isAutoUserEnabledMutex) { autoUserEnabled = false; } } /* package */ static boolean isAutomaticUserEnabled() { synchronized (isAutoUserEnabledMutex) { return autoUserEnabled; } } //endregion //region Legacy/Revocable Session Tokens /** * Enables revocable sessions. This method is only required if you wish to use * {@link EasimartSession} APIs and do not have revocable sessions enabled in your application * settings on parse.com. *

* Upon successful completion of this {@link Task}, {@link EasimartSession} APIs will be available * for use. * * @return A {@link Task} that will resolve when enabling revocable session */ public static Task enableRevocableSessionInBackground() { // TODO(mengyan): Right now there is no way for us to add interceptor for this client, // so maybe we should move add interceptor steps to restClient() EasimartCorePlugins.getInstance().registerUserController( new NetworkUserController(EasimartPlugins.get().restClient(), true)); return getCurrentUserController().getAsync(false).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { EasimartUser user = task.getResult(); if (user == null) { return Task.forResult(null); } return user.upgradeToRevocableSessionAsync(); } }); } /* package */ Task upgradeToRevocableSessionAsync() { return taskQueue.enqueue(new Continuation>() { @Override public Task then(Task toAwait) throws Exception { return upgradeToRevocableSessionAsync(toAwait); } }); } private Task upgradeToRevocableSessionAsync(Task toAwait) { final String sessionToken = getSessionToken(); return toAwait.continueWithTask(new Continuation>() { @Override public Task then(Task task) throws Exception { return EasimartSession.upgradeToRevocableSessionAsync(sessionToken); } }).onSuccessTask(new Continuation>() { @Override public Task then(Task task) throws Exception { String result = task.getResult(); return setSessionTokenInBackground(result); } }); } //endregion }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy