Friday, December 23, 2011

GWT RequestFactoryGenerator Mod: Session timeout handling on client without code refactor- when using Receivers and RequestFactory


Problem:
There's a GWT project that uses RequestFactory-entity-proxy-receiver mechanism to load data from server.(http://code.google.com/webtoolkit/doc/latest/DevGuideRequestFactory.html).
It's an alternative to GWT-RPC.
The objective is to make the client session-timeout-aware i.e. It should show a popup and redirect/load to a login endpoint if a data request can't be served due to session-timeout.

Most-intuitive solution:
If there is an entity with a method (eg: MyEntity.myMethod():MyReturnObject) then the GWT client makes a call to the method in a manner similar to following:

myEntityRequest = requestFactory.myEntityRequest();
myEntityRequest.myMethod().fire(
new Receiver<MyReturnObjectProxy>(){

public void onSuccess(MyReturnObjectProxy response)
{
this is success callback
}

@Override
public void onFailure(ServerFailure error)
{
this is failure callback. failure = thrown/unthrown exceptions, connection failures etc.
}

}

If you are wondering, myEntityRequest.myMethod() must be a stub implementation generated by GWT for myEntity.myMethod().

The most intuitive solution is to create a custom Receiver class that implements the session-timeout handling logic in its onFailure() and use it everywhere instead of the inline Receiver implementations as above.

Problem with the intuitive solution:
Well, there's already over 500 places in the code where inline receivers have already been declared.
This would mean a lo....t of refactoring.

Another Solution:
http://stackoverflow.com/questions/3657572/how-to-redirect-to-login-page-after-session-expire-in-gwt-rpc-call - has a smart solution discussed by "Piotr".
The idea is based on the fact that GWT offers deferred binding (http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsDeferred.html)
But the code provided in the post are for those using GWT-RPC.
We will be re-doing the same for RequestFactory.

The code:
It seems that com.google.gwt.requestfactory.rebind.RequestFactoryGenerator is responsible for emitting some intermediate JAVA code for the "requestFactory" that gets converted into JS eventually. And then you use this requestFactory to create *Request objects(eg: myEntityRequest)

However RequestFactoryGenerator is not as generous as MyRpcRemoteProxyGenerator(Refer Piotr's  changes in classes in his post). Most of the code is private. No mechansim to inject some MyProxyCreator.

So we copy the entire code and patch it.

The logic is exactly same as the one in the blog post by "Piotr".

Step1:
Create a com.foo.bar.client.requestfactory.shared.MyReceiver extends Receiver

public class MyReceiver<V> extends Receiver<V> {

private final Receiver<V> receiver;
private final String sessionExpiredExceptionType = MyConstants.SESSION_EXPIRED_EXCEPTION_TYPE;

public MyReceiver(Receiver<V> receiver) {
this.receiver = receiver;
}

@Override
public void onSuccess(V response) {
// Window.alert("Success");
receiver.onSuccess(response);
}

@Override
public void onFailure(ServerFailure error) {

if(null!= error.getExceptionType() && error.getExceptionType().equals(sessionExpiredExceptionType)){
Window.alert("Sorry! Session Expired due to inactivity.");
Window.Location.replace("login");
return;
}
receiver.onFailure(error);
}

@Override
public void onViolation(Set<Violation> errors) {
receiver.onViolation(errors);
}

}

Please note how elegantly this class composes and reuses the callbacks of the Receiver passed to its constructor. Yes, you are right. We won't be touching the existing inline Receivers at all. The Generator takes care of it all. Go through the Generator code to figure it out how it does this.

In short, for every *Request class it generates, it overrides the fire method - wraps the argument receiver into our custom receiver  - and performs super.fire(customReceiver).

Step2:
Add the Generator mod. Uploaded a diff at (http://www.esnips.com/displayimage.php?album=3521675&pid=33041663) Use the diff to patch a copy of com.google.gwt.requestfactory.rebind.RequestFactoryGenerator and rename it as some com.foo.bar.server.gwt.rebind.MyRequestFactoryGenerator.
If you want to DIY manually, look out for the tokens "@TAPOMAY". Compare the area of interest with original code and use the code that has the token as a prefix.

Step3:
Add the following deferred-binding declaration in your <Module>.gwt.xml
<generate-with
class="com.foo.bar.server.gwt.rebind.MyRequestFactoryGenerator">
<when-type-assignable class="com.google.gwt.requestfactory.shared.RequestFactory" />
</generate-with>
This enables GWT to use our custom generator to be used to emit the RequestFactoryGenerator code.

Step4:
Enjoy the modded behaviour

Step5:
The MyReceiver.onFailure() in step1 detects session timeout against other failures by checking the ServerFailure.exceptionType.
Following is a sample servlet that can serve such a JSON to the client. This servlet is used by the server-side security layer as a request-forward destination to serve AJAX(/gwtRequest) requests if there is a session timeout.

/**
 * Servlet for handling request forward on session timeout.
 * Used for customizing what is returned to client.
 *
 * If GET request: redirect to login page
 * If POST request: Assume this was a GWT AJAX request. Uses GWT's mechanism for generating
 * response JSON indicating a SessionAuthenticationException.
 * Sets the serverFailure.exceptionType so that client can differentiate SessionAuthenticationException from other failures.
 * The JSON creation mechanism was taken from
 * com.google.gwt.requestfactory.server.SimpleRequestProcessor.process()
 * and com.google.gwt.requestfactory.server.RequestFactoryServlet
 *
 * @author Tapomay Dey (tapomay.dey@texity.com)
 *
 */
public class SessionExpiredServlet extends HttpServlet {

public static final String exceptionType = MyConstants.SESSION_EXPIRED_EXCEPTION_TYPE;
static final MessageFactory FACTORY = AutoBeanFactoryMagic.create(MessageFactory.class);
private ExceptionHandler exceptionHandler = new DefaultExceptionHandler();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect(req.getContextPath() + "/login");
}

@SuppressWarnings("deprecation")
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse response)
throws ServletException, IOException {
String servletPath = req.getServletPath();
String requestURI = req.getRequestURI();
StringBuffer requestURL = req.getRequestURL();
PrintWriter writer = response.getWriter();
System.out.println(servletPath);

SessionAuthenticationException e = new SessionAuthenticationException("SessionAuthenticationException. Please login again.");

AutoBean<ResponseMessage> responseBean = FACTORY.response();
AutoBean<ServerFailureMessage> createFailureMessage = createFailureMessage(e);

ServerFailureMessage serverFailureMessage = createFailureMessage.as();
ResponseMessage responseMessage = responseBean.as();
responseMessage.setGeneralFailure(serverFailureMessage);

String payload = AutoBeanCodex.encode(responseBean).getPayload();

writer.print(payload);
writer.flush();
}

 private AutoBean<ServerFailureMessage> createFailureMessage(
     Exception e) {
   ServerFailure failure = exceptionHandler.createServerFailure(e.getCause() == null
       ? e : e.getCause());
   AutoBean<ServerFailureMessage> bean = FACTORY.failure();
   ServerFailureMessage msg = bean.as();
   msg.setExceptionType(exceptionType);
   msg.setMessage(failure.getMessage());
   msg.setStackTrace(failure.getStackTraceString());
   msg.setFatal(failure.isFatal());
   return bean;
 }

}