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

com.cedarsolutions.client.gwt.rpc.util.AbstractRpcCaller Maven / Gradle / Ivy

There is a newer version: 5.8.4
Show newest version
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 *              C E D A R
 *          S O L U T I O N S       "Software done right."
 *           S O F T W A R E
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 * Copyright (c) 2013 Kenneth J. Pronovici.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Apache License, Version 2.0.
 * See LICENSE for more information about the licensing terms.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 * Author   : Kenneth J. Pronovici 
 * Language : Java 6
 * Project  : Common Java Functionality
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package com.cedarsolutions.client.gwt.rpc.util;

import com.cedarsolutions.client.gwt.rpc.proxy.XsrfRpcProxyConfig;
import com.cedarsolutions.exception.InvalidDataException;
import com.cedarsolutions.exception.RpcSecurityException;
import com.cedarsolutions.shared.domain.ErrorDescription;
import com.cedarsolutions.util.gwt.GwtTimer;
import com.cedarsolutions.web.metadata.HttpStatusCode;
import com.google.gwt.http.client.RequestTimeoutException;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RpcTokenException;
import com.google.gwt.user.client.rpc.StatusCodeException;

/**
 * RPC caller that a callback interacts with.
 *
 * 

* The goal here is to apply behavioral policies to RPCs on a system-wide * basis, rather than pushing those decisions out to all of the individual RPC * invocations. For instance, we can apply a system-wide RPC timeout policy, * and we have a place to handle specific errors, retry failed requests, and * log information in a consistent way across all RPCs. *

* *

* The resulting RPC invocation idiom looks a little unsual compared to * "typical" example GWT code. Client code never directly invokes RPCs. * However, since I prefer to avoid anonymous class implementations anyway for * testing purposes, this code already looked fairly unusual. It's not a * perfect system, but it gets me enough functional benefits that I'm happy * with it. *

* *

* This abstract class tries to standardize behavior as much as possible, while * delegating application decisions (like how to actually display errors) to a * concrete class in the actual application. *

* * @param Type of the asynchronous RPC * @param Return type of the RPC method * * @author Kenneth J. Pronovici */ public abstract class AbstractRpcCaller implements IRpcCaller { /** The asynchronous RPC. */ private A async; /** Name of the RPC that is being invoked, like "IClientSessionRpc". */ private String rpc; /** Name of the method that is being invoked, like "establishClientSession". */ private String method; /** Number of attempts made so far. */ private int attempts; /** The caller id that's currently set. */ private String callerId; /** Maximum number of attempts allowed for this RPC method. */ private int maxAttempts; /** Elapsed timer. */ private GwtTimer timer; /** * Create an RPC caller. * @param async The asynchronous RPC * @param rpc Name of the RPC that is being invoked, like "IClientSessionRpc" * @param method Name of the method that is being invoked, like "establishClientSession" */ protected AbstractRpcCaller(A async, String rpc, String method) { this.async = async; this.rpc = rpc; this.method = method; this.attempts = 0; this.callerId = this.getNextCallerId(); this.maxAttempts = 1; // by default, calls are not retried } /** Apply global policies that are required for all RPCs. */ protected void applyGlobalPolicies() { // This is hideous. I'm setting a global property every single time an // RPC is invoked. Unfortunately, there's no other obvious way to // inject configuration into the XsrfRpcProxy class that we're using // underneath. On the positive side, client-side code is // single-threaded, so there's no real chance for thread conflict. // If callers set different values for different RPC invocations, // they'll probably get what they expect. XsrfRpcProxyConfig.getInstance().setTimeoutMs(this.getXsrfRpcProxyTimeoutMs()); } /** Sets the global timeout to be used by the XSRF RPC proxy, or null to use the default. */ public abstract int getXsrfRpcProxyTimeoutMs(); /** Hook that lets child classes apply policies to an RPC. */ public abstract void applyPolicies(); /** Invoke the RPC method with the correct arguments, using the passed-in callback. */ public abstract void invokeRpcMethod(A async, AsyncCallback callback); /** Get the next caller id from some application-wide id manager, or null. */ public abstract String getNextCallerId(); /** Show an error to the user. */ public abstract void showError(ErrorDescription error); /** Generate an error due to an authorization problem like HTTP FORBIDDEN. */ public abstract ErrorDescription generateNotAuthorizedError(HttpStatusCode statusCode); /** Generate an error due to an RpcSecurityException. */ public abstract ErrorDescription generateRpcSecurityExceptionError(RpcSecurityException exception); /** Generate an error due to an RpcTokenException. */ public abstract ErrorDescription generateRpcTokenExceptionError(RpcTokenException exception); /** Generate an error due to a RequestTimeoutException. */ public abstract ErrorDescription generateRequestTimeoutExceptionError(RequestTimeoutException exception); /** Generate an error due to no response received. */ public abstract ErrorDescription generateNoResponseReceivedError(Throwable exception); /** Generate an error due to an IncompatibleRemoteServiceException. */ public abstract ErrorDescription generateIncompatibleRemoteServiceExceptionError(IncompatibleRemoteServiceException exception); /** Generate an error due to a general exception. */ public abstract ErrorDescription generateGeneralRpcError(Throwable exception); /** Generate an error due to a general exception that resulted in an HTTP error. */ public abstract ErrorDescription generateGeneralRpcError(Throwable exception, HttpStatusCode statusCode); /** Get this caller's descriptive identifier. */ @Override public String getDescriptiveCallerId() { return "RPC [" + this.getCallerId() + "] " + this.rpc + "." + this.method + "()"; } /** Get the RPC method that is being invoked. */ public String getRpcMethod() { return this.rpc + "." + this.method + "()"; } /** * Create an RPC callback of the proper type. * Child classes can override this to use a specialized type of callback. */ public IRpcCallback createCallback() { return new RpcCallback(this); } /** * Whether another RPC method call attempt should be made. * @param caught Exception that caused the situation * @return True if retry should be made, false otherwise. */ @Override public boolean isAnotherAttemptAllowed(Throwable caught) { return isExceptionRetryable(caught) ? this.attempts < this.maxAttempts : false; } /** Invoke the RPC method with the proper arguments, creating a new callback. */ @Override public void invoke() { AsyncCallback callback = this.createCallback(); this.invoke(callback); } /** * Invoke the RPC method with the proper arguments, using the passed-in callback. * Callers are expected to check in with isAnotherAttemptAllowed() before invoking this method. * @param callback Callback to use when invoking the RPC method. */ @Override public void invoke(AsyncCallback callback) { this.applyGlobalPolicies(); this.applyPolicies(); this.showProgressIndicator(); this.incrementAttempts(); this.startTimer(); this.log(this.getDescriptiveCallerId() + ": start" + this.getDescriptiveCallState()); this.invokeRpcMethod(this.async, callback); } /** * Method invoked when an asynchronous call results in a validation error. * Child classes can override this method to implement specific validation behavior. * @param caught Validation error that was caught * @return True if the validation error was handled, false otherwise. */ @Override public boolean onValidationError(InvalidDataException caught) { return false; } /** * Method invoked when an asynchronous call results in an unhandled error. * Child classes can override this method to implement specific error-handling behavior. * @param caught Unhandled error that was caught */ @Override public void onUnhandledError(Throwable caught) { boolean handled = this.handleSpecialErrors(caught); if (!handled) { ErrorDescription error = this.generateError(caught); this.showError(error); } } /** * Hook that lets child classes handle particular exceptions in a special way. * *

* By default, this is a no-op. Child classes can override the method, * implement their own behavior for specific exceptions, and return true if * the exception was handled in a special way. A typical use case might be * to handle the non-standard AUTHENTICATION_TIMEOUT status and redirect the * user to a login page rather than showing them an RPC error. *

* * @param caught Unhandled error that was caught * * @return True if the exception has been handled, false to follow the existing error-handling logic. */ public boolean handleSpecialErrors(Throwable caught) { return false; // default implementation is a no-op } /** Get descriptive call state, like "(1 of 2 attempts)", possibly empty. */ @Override public String getDescriptiveCallState() { return this.maxAttempts > 1 ? " (attempt " + this.attempts + " of " + this.maxAttempts + ")" : ""; } /** Get the elapsed time for the current call, as a string. */ @Override public String getElapsedTime() { if (this.timer != null) { this.timer.stop(); return ", elapsed: " + this.timer.getElapsedTimeString(); } else { return ""; } } /** Start the GWT timer. */ protected void startTimer() { this.timer = new GwtTimer(); this.timer.start(); } /** Increment the number of of attempts that have been made. */ protected void incrementAttempts() { this.attempts += 1; } /** * Generate the correct error for a caught exception. * *

* Child classes can override this if there are other application-specific * exceptions they want to handle in specific ways. The pattern to follow * is something like: *

* *
     * try {
     *    throw caught;
     * } catch (MySpecialException exception) {
     *    return new MySpecialErrorDescription(exception);
     * } catch (Throwable exception) {
     *    return super.generateError(exception);
     * }
     * 
* *

* This probably isn't that common of a use-case, but it is safe to do it * if necessary. *

* * @param caught Exception to check * @return Error description for the passed-in exception. */ public ErrorDescription generateError(Throwable caught) { try { throw caught; } catch (StatusCodeException exception) { if (exception.getStatusCode() == 0) { // No idea why a status code of zero means "nothing received"...? return this.generateNoResponseReceivedError(exception); } else { HttpStatusCode statusCode = HttpStatusCode.convert(exception.getStatusCode()); switch(statusCode) { case UNAUTHORIZED: case FORBIDDEN: return this.generateNotAuthorizedError(statusCode); default: return this.generateGeneralRpcError(exception, statusCode); } } } catch (RpcSecurityException exception) { return this.generateRpcSecurityExceptionError(exception); } catch (RpcTokenException exception) { return this.generateRpcTokenExceptionError(exception); } catch (RequestTimeoutException exception) { return this.generateRequestTimeoutExceptionError(exception); } catch (IncompatibleRemoteServiceException exception) { return this.generateIncompatibleRemoteServiceExceptionError(exception); } catch (Throwable exception) { return this.generateGeneralRpcError(exception); } } /** * Whether an exception is retryable. * *

* Child classes can override this if there are other application-specific * exceptions they want to retry. The pattern to follow is something like: *

* *
     * try {
     *    throw caught;
     * } catch (MySpecialException exception) {
     *    return true;
     * } catch (Throwable exception) {
     *    return super.isExceptionRetryable(caught);
     * }
     * 
* *

* Think carefully before you make other exceptions retryable. The best * candidates are exceptions where you can reliably tell that a retry * will make a difference (i.e. you can tell that the network dropped * but if it came back you might get a different result). *

* *

* In the default implementation, only RequestTimeoutException is retried. * This exception indicates a timeout invoking an RPC. I considered handling * InvocationException in a special way, because it's documented to happen * if the RPC can't be invoked at all. However, InvocationException can also * occur for unrelated reasons (mainly undeclared exceptions thrown from the * server-side service layer), so I've decided to ignore it for now. *

* * @param caught Exception to check * @return True if the exception is retryable, false otherwise. * * @see
DevGuideServerCommunication */ public boolean isExceptionRetryable(Throwable caught) { try { throw caught; } catch (RequestTimeoutException exception) { return true; } catch (Throwable exception) { return false; } } public A getAsync() { return this.async; } public String getRpc() { return this.rpc; } public String getMethod() { return this.method; } public int getAttempts() { return this.attempts; } public String getCallerId() { return this.callerId; } public int getMaxAttempts() { return this.maxAttempts; } public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy