![JAR search and dependency download from the Maven repository](/logo.png)
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