/* * Copyright (c) 2009-2011 Lockheed Martin Corporation * * 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 org.eurekastreams.commons.client; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.eurekastreams.commons.exceptions.SessionException; import com.google.gwt.user.client.rpc.AsyncCallback; /** * The queuable action processor takes in requests to the action rpc service. It has the ability to queue up requests * and send them as one request instead of multiple. This is used on page init to save on HTTP transfers. */ @SuppressWarnings("rawtypes") public class ActionProcessorImpl implements ActionProcessor { /** Log. */ private final Logger log = Logger.getLogger("ActionProcessorImpl"); /** The RPC service to call. */ private final ActionRPCServiceAsync service; /** For generating request IDs. */ private int lastRequestId = 0; /** Ordered index of all requests. */ private final Map<Integer, RequestInfo> requestIndex = new LinkedHashMap<Integer, RequestInfo>(); /** The queue of action requests. */ private final List<RequestInfo> requestQueue = new ArrayList<RequestInfo>(); /** Current session id. */ private String sessionId = null; /** Current number of outstanding message bundles. */ private int outstandingMessages = 0; /** If normal processing is suspended to establish a session with the server. */ private boolean establishingSession = false; /** If requests should be held in the queue (to allow batching). */ private boolean holdQueue = false; /** Callback to notify app of session establish success/failure. */ private final AsyncCallback<String> appEstablishSessionCallback; /** * Standard constructor. Takes in an Action RPC Service object. * * @param inService * the action rpc service to use. * @param inAppEstablishSessionCallback * Callback to notify app of session establish success/failure. */ public ActionProcessorImpl(final ActionRPCServiceAsync inService, final AsyncCallback<String> inAppEstablishSessionCallback) { service = inService; appEstablishSessionCallback = inAppEstablishSessionCallback; } /** * Handling for success and failure of establishing a session. */ private final AsyncCallback<String> establishSessionCallback = new AsyncCallback<String>() { public void onSuccess(final String inResult) { sessionId = inResult; establishingSession = false; log.fine("Session established: " + sessionId); // let app know if it's interested if (appEstablishSessionCallback != null) { try { appEstablishSessionCallback.onSuccess(inResult); } catch (Exception ex) { log.log(Level.WARNING, "Unhandled exception in application session established callback", ex); } } // session is now established, so send any requests sendRequests(requestIndex.values()); } public void onFailure(final Throwable inCaught) { establishingSession = false; log.fine("Error establishing session: " + inCaught); // Let app know about failure so it can try to do something about it if (appEstablishSessionCallback != null) { try { appEstablishSessionCallback.onFailure(inCaught); } catch (Exception ex) { log.log(Level.WARNING, "Unhandled exception in application session establishment failure callback", ex); } } } }; /** * Initiates establishing of a session. */ private void establishSession() { log.finest("Entering establishSession"); // wait until all request batches have returned, and don't send out multiple session requests if (outstandingMessages > 0 || establishingSession) { return; } log.fine("About to establish session"); // discard the queue, since we will re-request using the index after the session is established requestQueue.clear(); // the outstanding message count SHOULD already be zero, but if it's not (namely if it went negative somehow), // we'd like to not have this whole thing hang outstandingMessages = 0; establishingSession = true; try { service.establishSession(establishSessionCallback); } catch (Exception ex) { establishSessionCallback.onFailure(ex); } } /** * Attempts to send any queued actions. */ public void fireQueuedRequests() { log.finest("Entering fireQueuedRequests"); // establish session instead if (sessionId == null) { establishSession(); return; } // send the messages sendRequests(requestQueue); } /** * Builds and sends a batch of requests to the server. * * @param requestSource * Where to get the list of requests from. */ private void sendRequests(final Collection<RequestInfo> requestSource) { log.finest("Entering sendRequests"); if (requestSource.isEmpty()) { return; } log.fine("sending " + requestSource.size() + " requests."); // prepare list to send // Note: we really only need one session id per batch (since it's one HTTP request), so we could set it only // on the first request batch and then only check it on the first request of the batch in ActionRPCServiceImpl final ActionRequest[] requests = new ActionRequest[requestSource.size()]; int i = 0; for (RequestInfo info : requestSource) { requests[i] = info.getRequest(); requests[i].setSessionId(sessionId); i++; } // clear the queue // requestSource may not be the queue - it may be the index - but we know that either way, all messages that // were in the queue will have been sent, since everything that is in the queue is also in the index. requestQueue.clear(); // send the message and handle results // Note: The onFailure method of the callback can be called inline by service.execute on certain errors. If that // occurred AND onSendBatchFailure also threw an exception AND since service.execute doesn't catch the // exceptions from callbacks (at least in GWT 2.2 with a draft-mode compile - I checked the generated // JavaScript), then outstandingMessages would get decremented twice and onSendBatchFailure would be called // twice; this is being prevented by the try/catch block around the call to onSendBatchFailure. // onSendBatchSuccess is also protected just for good measure. try { outstandingMessages++; service.execute(requests, new AsyncCallback<ActionRequest[]>() { public void onSuccess(final ActionRequest[] inResult) { outstandingMessages--; try { onSendBatchSuccess(inResult); } catch (Exception ex) { log.log(Level.WARNING, "Unhandled exception in onSendBatchSuccess", ex); } } public void onFailure(final Throwable inCaught) { outstandingMessages--; try { onSendBatchFailure(requests, inCaught); } catch (Exception ex) { log.log(Level.WARNING, "Unhandled exception in onSendBatchFailure", ex); } } }); } catch (Exception ex) { outstandingMessages--; onSendBatchFailure(requests, ex); } } /** * Handles a "successful" batch of requests - namely the HTTP interaction worked. There may be a session exception * (stored in each response) or individual responses may have failed. * * @param responses * The responses from the server. */ @SuppressWarnings("unchecked") private void onSendBatchSuccess(final ActionRequest[] responses) { log.fine("Batch success with " + responses.length + " responses."); for (ActionRequest response : responses) { // ideally, we'd have a class representing the entire batch and batch-level exceptions (like the session // exception) would come back in there, but since we don't, we have to check on individual requests if (response.getResponse() instanceof SessionException) { log.fine("Found a SessionException in the response batch."); sessionId = null; } else { RequestInfo info = requestIndex.remove(response.getId()); log.finer("Done with request " + response.getId() + " (" + (info == null ? "<NOT FOUND>" : info.getRequest().getActionKey()) + ") with result " + response.getResponse()); if (info != null && info.getCallback() != null) { try { if (response.getResponse() instanceof Throwable) { info.getCallback().onFailure((Throwable) response.getResponse()); } else { info.getCallback().onSuccess(response.getResponse()); } } catch (Exception ex) { // swallow exceptions from callbacks - a fault in a callback shouldn't break everything else log.log(Level.WARNING, "Unhandled exception in application action request callback for " + info.getRequest().getActionKey(), ex); } catch (AssertionError ex) { // Note: Catching AssertionErrors is good and bad. This means unit test assertion failures will // be swallowed and the test will not fail (bad), but any failed asserts within an app callback // will not break everything else. log.log(Level.SEVERE, "Unhandled exception in application action request callback for " + info.getRequest().getActionKey(), ex); } } } } // could have discovered above that the session was lost - re-establish it (when the last outstanding message // comes in, but establishSession will check that for us) if (sessionId == null) { establishSession(); } } /** * Handles failure of a batch of requests - this may be sync (failed to get it "out the door" such as serialization * failure) or async (error from the HTTP interaction). * * @param requests * The batch of requests that was being sent. * @param caught * The exception. */ private void onSendBatchFailure(final ActionRequest[] requests, final Throwable caught) { log.fine("Batch failure with exception " + caught); // handle batch-level exceptions (of which session failure is the only one we're dealing with) if (caught instanceof SessionException) { sessionId = null; // note: establishSession will insure there are no outstanding messages establishSession(); } else { // any exception other than a session exception gets distributed to each request // TODO: Come up with a better idea than this. Most batch-level exceptions are communications exceptions // which really should be handled at the comm level, not at the individual action level. Plus most action // requesters don't handle failure cases anyway, so an error causes the app to just sit there, usually with // an eternal spinner. Would be better to have this handled in a single place in a consistent way. for (ActionRequest request : requests) { RequestInfo info = requestIndex.remove(request.getId()); if (info != null && info.getCallback() != null) { try { info.getCallback().onFailure(caught); } catch (Exception ex) { // swallow exceptions from callbacks - a fault in a callback shouldn't break everything else log.log(Level.WARNING, "Unhandled exception in application action request callback for " + info.getRequest().getActionKey(), ex); } catch (AssertionError ex) { // Note: Catching AssertionErrors is good and bad. This means unit test assertion failures will // be swallowed and the test will not fail (bad), but any failed asserts within an app callback // will not break everything else. log.log(Level.SEVERE, "Unhandled exception in application action request callback for " + info.getRequest().getActionKey(), ex); } } } } } /** * Makes a request to the action rpc service. If the action processor is queueable then it adds it to the queue and * waits until the FireQueuedRequests method is called * * @param actionKey * - identify the action to load. * @param param * - parameter to pass to the action during execution. * @param callback * the AsyncCallback to call after the request is handled. Please provide an OnFailure and an OnSuccess */ public void makeRequest(final String actionKey, final Serializable param, final AsyncCallback callback) { log.finer("Make request for " + actionKey); int id = ++lastRequestId; RequestInfo info = new RequestInfo(id, actionKey, param, callback); requestIndex.put(id, info); requestQueue.add(info); if (!holdQueue) { fireQueuedRequests(); } } /** * Turns the queue on or off. * * @param queue * The value for enabling or disabling queue. */ public void setQueueRequests(final boolean queue) { holdQueue = queue; // send any queued requests when turning off queuing (since callers are likely to forget) if (!holdQueue) { fireQueuedRequests(); } } /* -------- Nested implementation classes -------- */ /** * Holds information about a request. */ private static class RequestInfo { /** The request to send. */ private final ActionRequest< ? extends Serializable> request; /** The requestor's callback. */ private final AsyncCallback callback; /** * Constructor. * * @param inId * ID of the request. * @param inActionKey * Name of the action to perform. * @param inParam * Parameter to be passed to the ServiceAction. * @param inCallback * Requestor's callback. */ @SuppressWarnings("deprecation") public RequestInfo(final int inId, final String inActionKey, final Serializable inParam, final AsyncCallback inCallback) { request = new ActionRequestImpl<Serializable>(inActionKey, inParam); request.setId(inId); callback = inCallback; } /** * @return the request */ public ActionRequest< ? extends Serializable> getRequest() { return request; } /** * @return the callback */ public AsyncCallback getCallback() { return callback; } } }