package org.activityinfo.promise; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.collect.Lists; import com.google.gwt.user.client.rpc.AsyncCallback; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * The Promise interface represents a proxy for a value not necessarily known at its creation time. * It allows you to associate handlers to an asynchronous action's eventual success or failure. * This let asynchronous methods to return values like synchronous methods: instead of the final value, * the asynchronous method returns a promise of having a value at some point in the future. * * @param <T> the type of the promised value */ public final class Promise<T> implements AsyncCallback<T> { public enum State { /** * The action relating to the promise succeeded */ FULFILLED, /** * The action relating to the promise failed */ REJECTED, /** * Hasn't fulfilled or rejected yet */ PENDING } private State state = State.PENDING; private T value; private Throwable exception; private List<AsyncCallback<? super T>> callbacks = null; public Promise() { } public State getState() { return state; } public boolean isSettled() { return state == State.FULFILLED || state == State.REJECTED; } public final void resolve(T value) { if (state != State.PENDING) { return; } this.value = value; this.state = State.FULFILLED; publishFulfillment(); } public void then(AsyncCallback<? super T> callback) { switch (state) { case PENDING: if (callbacks == null) { callbacks = Lists.newArrayList(); } callbacks.add(callback); break; case FULFILLED: callback.onSuccess(value); break; case REJECTED: callback.onFailure(exception); break; } } public <R> Promise<R> join(final Function<? super T, Promise<R>> function) { final Promise<R> chained = new Promise<R>(); then(new AsyncCallback<T>() { @Override public void onFailure(Throwable caught) { chained.onFailure(caught); } @Override public void onSuccess(T t) { try { function.apply(t).then(chained); } catch(Throwable caught) { chained.onFailure(caught); } } }); return chained; } public Promise<Void> thenDiscardResult() { return then(Functions.<Void>constant(null)); } public <R> Promise<R> join(Supplier<Promise<R>> supplier) { return join(Functions.forSupplier(supplier)); } /** * Provides state updates to the given monitor. * @return {@code this}, for method chaining */ public Promise<T> withMonitor(final PromiseMonitor monitor) { monitor.onPromiseStateChanged(state); if(state == State.PENDING) { then(new AsyncCallback<T>() { @Override public void onFailure(Throwable caught) { monitor.onPromiseStateChanged(State.REJECTED); } @Override public void onSuccess(T result) { monitor.onPromiseStateChanged(State.FULFILLED); } }); } return this; } /** * Transforms a binary function {@code (T, U) → R} to a function which operates on the equivalent * Promised values: {@code ( Promise<T>, Promise<U> ) → Promise<R> } */ public static <T, U, R> BiFunction<Promise<T>, Promise<U>, Promise<R>> fmap(final BiFunction<T, U, R> function) { return new BiFunction<Promise<T>, Promise<U>, Promise<R>>() { @Override public Promise<R> apply(final Promise<T> promiseT, final Promise<U> promiseU) { Preconditions.checkNotNull(promiseT, "promise cannot be null"); return promiseT.join(new Function<T, Promise<R>>() { @Nullable @Override public Promise<R> apply(@Nullable T t) { return promiseU.then(function.apply(t)); } }); } }; } public static <T, R> Promise<List<R>> map(Iterable<T> items, Function<T, Promise<R>> function) { List<Promise<List<R>>> promisedItems = Lists.newArrayList(); for(T item : items) { promisedItems.add(function.apply(item).then(Functions2.<R>singletonList())); } // we need a concat function that will take two [ Promise<List<R>> Promise<List<R>> -> Promise<List<R>> ]\ BiFunction<Promise<List<R>>, Promise<List<R>>, Promise<List<R>>> concatOp = fmap(new ConcatList<R>()); Promise<List<R>> initialValue = Promise.<List<R>>resolved(new ArrayList<R>()); return BiFunctions.foldLeft(initialValue, concatOp, promisedItems); } /** * Convenience function for applying an fmap'd foldLeft to a list of Promises. */ public static <T> Promise<T> foldLeft(T initialValue, BiFunction<T, T, T> operator, Iterable<Promise<T>> promises) { return BiFunctions.foldLeft(Promise.resolved(initialValue), fmap(operator), promises); } /** * Convenience function for concatenating a promised item with a promised list of items of the same type */ public static <T> Promise<List<T>> prepend(Promise<T> a, Promise<List<T>> b) { Promise<List<T>> aList = a.then(Functions2.<T>singletonList()); return fmap(new ConcatList<T>()).apply(aList, b); } /** * * @param function * @param <R> * @return */ public <R> Promise<R> then(final Function<? super T, R> function) { final Promise<R> chained = new Promise<R>(); then(new AsyncCallback<T>() { @Override public void onFailure(Throwable caught) { chained.reject(caught); } @Override public void onSuccess(T t) { try { chained.resolve(function.apply(t)); } catch (Throwable caught) { chained.reject(caught); } } }); return chained; } public <R> Promise<R> then(final Supplier<R> function) { return then(Functions.forSupplier(function)); } public T get() { if(state != State.FULFILLED) { throw new IllegalStateException(); } return value; } public Throwable getException() { if(state != State.REJECTED) { throw new IllegalStateException(); } return exception; } @Override public void onFailure(Throwable caught) { reject(caught); } @Override public void onSuccess(T result) { resolve(result); } public final void reject(Throwable caught) { if (state != State.PENDING) { return; } this.exception = caught; this.state = State.REJECTED; publishRejection(); } private void publishRejection() { if (callbacks != null) { for (AsyncCallback<? super T> callback : callbacks) { callback.onFailure(exception); } } } private void publishFulfillment() { if (callbacks != null) { for (AsyncCallback<? super T> callback : callbacks) { callback.onSuccess(value); } } } public static <T> Promise<T> resolved(T value) { Promise<T> promise = new Promise<T>(); promise.resolve(value); return promise; } public static Promise<Void> done() { return Promise.resolved(null); } public static <X> Promise<X> rejected(Throwable exception) { Promise<X> promise = new Promise<X>(); promise.reject(exception); return promise; } /** * Applies an asynchronous function to each of the elements in {@code items}, * @param items * @param function * @param <T> * @return */ public static <T> Promise<Void> forEach(Iterable<T> items, final Function<? super T, Promise<Void>> function) { Promise<Void> promise = Promise.resolved(null); for(final T item : items) { promise = promise.join(new Function<Void, Promise<Void>>() { @Nullable @Override public Promise<Void> apply(@Nullable Void input) { return function.apply(item); } }); } return promise; } public static Promise<Void> waitAll(Promise<?>... promises) { return waitAll(Arrays.asList(promises)); } public static Promise<Void> waitAll(final List<? extends Promise<?>> promises) { if(promises.isEmpty()) { return Promise.done(); } final Promise<Void> result = new Promise<>(); final int[] remaining = new int[] { promises.size() }; AsyncCallback callback = new AsyncCallback() { @Override public void onFailure(Throwable caught) { result.onFailure(caught); } @Override public void onSuccess(Object o) { remaining[0]--; if(remaining[0] == 0) { result.onSuccess(null); } } }; for(int i=0;i!=promises.size();++i) { promises.get(i).then(callback); } return result; } @Override public String toString() { switch(state) { case FULFILLED: return "<fulfilled: " + value + ">"; case REJECTED: return "<rejected: " + exception.getClass().getSimpleName() + ">"; default: case PENDING: return "<pending>"; } } }