// Copyright (C) 2013 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.client.rpc; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Class for grouping together callbacks and calling them in order. * * <p>Callbacks are added to the group with {@link #add(AsyncCallback)}, which returns a wrapped * callback suitable for passing to an asynchronous RPC call. The last callback must be added using * {@link #addFinal(AsyncCallback)} or {@link #done()} must be invoked. * * <p>The enclosing group buffers returned results and ensures that {@code onSuccess} is called * exactly once for each callback in the group, in the same order that callbacks were added. This * allows callers to, for example, use a {@link ScreenLoadCallback} as the last callback in the list * and only display the screen once all callbacks have succeeded. * * <p>In the event of a failure, the <em>first</em> caught exception is sent to <em>all</em> * callbacks' {@code onFailure} methods, in order; subsequent successes or failures are all ignored. * Note that this means {@code onFailure} may be called with an exception unrelated to the callback * processing it. */ public class CallbackGroup { private final List<CallbackGlue> callbacks; private final Set<CallbackGlue> remaining; private boolean finalAdded; private boolean failed; private Throwable failedThrowable; public static <T> Callback<T> emptyCallback() { return new Callback<T>() { @Override public void onSuccess(T result) {} @Override public void onFailure(Throwable err) {} }; } public CallbackGroup() { callbacks = new ArrayList<>(); remaining = new HashSet<>(); } public <T> Callback<T> addEmpty() { Callback<T> cb = emptyCallback(); return add(cb); } public <T> Callback<T> add(final AsyncCallback<T> cb) { checkFinalAdded(); return handleAdd(cb); } public <T> HttpCallback<T> add(HttpCallback<T> cb) { checkFinalAdded(); return handleAdd(cb); } public <T> Callback<T> addFinal(final AsyncCallback<T> cb) { checkFinalAdded(); finalAdded = true; return handleAdd(cb); } public <T> HttpCallback<T> addFinal(final HttpCallback<T> cb) { checkFinalAdded(); finalAdded = true; return handleAdd(cb); } public void done() { finalAdded = true; apply(); } public void addListener(AsyncCallback<Void> cb) { if (!failed && finalAdded && remaining.isEmpty()) { cb.onSuccess(null); } else { handleAdd(cb).onSuccess(null); } } public void addListener(CallbackGroup group) { addListener(group.<Void>addEmpty()); } private void success(CallbackGlue cb) { remaining.remove(cb); apply(); } private <T> void failure(CallbackGlue w, Throwable caught) { if (!failed) { failed = true; failedThrowable = caught; } remaining.remove(w); apply(); } private void apply() { if (finalAdded && remaining.isEmpty()) { if (failed) { for (CallbackGlue cb : callbacks) { cb.applyFailed(); } } else { for (CallbackGlue cb : callbacks) { cb.applySuccess(); } } callbacks.clear(); } } private <T> Callback<T> handleAdd(AsyncCallback<T> cb) { if (failed) { cb.onFailure(failedThrowable); return emptyCallback(); } CallbackImpl<T> wrapper = new CallbackImpl<>(cb); callbacks.add(wrapper); remaining.add(wrapper); return wrapper; } private <T> HttpCallback<T> handleAdd(HttpCallback<T> cb) { if (failed) { cb.onFailure(failedThrowable); return new HttpCallback<T>() { @Override public void onSuccess(HttpResponse<T> result) {} @Override public void onFailure(Throwable caught) {} }; } HttpCallbackImpl<T> w = new HttpCallbackImpl<>(cb); callbacks.add(w); remaining.add(w); return w; } private void checkFinalAdded() { if (finalAdded) { throw new IllegalStateException("final callback already added"); } } public interface Callback<T> extends AsyncCallback<T>, com.google.gwtjsonrpc.common.AsyncCallback<T> {} private interface CallbackGlue { void applySuccess(); void applyFailed(); } private class CallbackImpl<T> implements Callback<T>, CallbackGlue { AsyncCallback<T> delegate; T result; CallbackImpl(AsyncCallback<T> delegate) { this.delegate = delegate; } @Override public void onSuccess(T value) { this.result = value; success(this); } @Override public void onFailure(Throwable caught) { failure(this, caught); } @Override public void applySuccess() { AsyncCallback<T> cb = delegate; if (cb != null) { delegate = null; cb.onSuccess(result); result = null; } } @Override public void applyFailed() { AsyncCallback<T> cb = delegate; if (cb != null) { delegate = null; result = null; cb.onFailure(failedThrowable); } } } private class HttpCallbackImpl<T> implements HttpCallback<T>, CallbackGlue { private HttpCallback<T> delegate; private HttpResponse<T> result; HttpCallbackImpl(HttpCallback<T> delegate) { this.delegate = delegate; } @Override public void onSuccess(HttpResponse<T> result) { this.result = result; success(this); } @Override public void onFailure(Throwable caught) { failure(this, caught); } @Override public void applySuccess() { HttpCallback<T> cb = delegate; if (cb != null) { delegate = null; cb.onSuccess(result); result = null; } } @Override public void applyFailed() { HttpCallback<T> cb = delegate; if (cb != null) { delegate = null; result = null; cb.onFailure(failedThrowable); } } } }