Saturday, February 13, 2010

Flash Scope alternative in Spring MVC

Flash scope in Spring MVC has been a topic of discussion, and even though Spring provides infrastructure to create custom scopes, I went with another solution. Hey.. as long as it works.. (for your requirements and within your constraints)...

The goal:
Set a flash error or flash message before redirecting to another view. The next view consumes the flash, displays it and clears it (so that on further redirects or refreshes the message does not appear).

Here we go:

1. Create a SessionHandler (or any other dependency you can inject into your Controller).

public class SessionHandler {

private transient final Log log = LogFactory.getLog(SessionHandler.class);

public static final String FLASH_MESSAGE = "FLASH_MESSAGE";
public static final String FLASH_ERROR = "FLASH_ERROR";
public static final String FLASH_TYPE = "flashType";

public void flashError(HttpServletRequest request, String message){
flash(request, FLASH_ERROR, message);
}

public void flashMessage(HttpServletRequest request, String message){
flash(request, FLASH_MESSAGE, message);
}

public String consumeFlash(HttpServletRequest request, String flashType){
String flash = null;
try {
flash = (String) request.getSession().getAttribute(flashType);
request.getSession().removeAttribute(flashType);
} catch (Exception e){
log.warn("Error retrieving flash value of type:" + flashType);
log.error(e.getMessage());
}
return flash;
}


/**
* helper method that sets the flash message of the type provided in session scope
*/
private void flash(HttpServletRequest request, String flashType, String message){
request.getSession().setAttribute(flashType, message);
}

This handler accepts the flash error/message and sets in session. When requested for it (using consumeFlash), it gets it from the session and clears it. It returns null if it does not find it in session.Next,

2. Use it in your controller.

if(<error condition>){
sessionHandler.flashError(request, "user.error.user.id.invalid");
return new ModelAndView(cancelView);
}
or on a successful operation,
userManager.save(user);
sessionHandler.flashMessage(request, "user.message.saved.successfully");
return new ModelAndView(getSuccessView());
As of this point we can flash an error or a message. Next step is to show it. Here is where my solution gets a bit different. I created another controller (FlashController) that creates a view of type MappingJacksonJsonView and returns that along with flash messages as redereredAttributes.

3. Create FlashController (which is ResourceLoaderAware) and map it to a url (say /flash.html)

public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {

MappingJacksonJsonView view = new MappingJacksonJsonView();
Map model = new HashMap();
Resource resource = null;
PropertyResourceBundle propertyResourceBundle = null;

try{
resource = resourceLoader.getResource(resourceLocation);
propertyResourceBundle = new PropertyResourceBundle(resource.getInputStream());
}catch (Exception e){
log.warn("Error loading resource bundle");
log.error(e.getMessage());
}

String flashError = sessionHandler.consumeFlash(request, SessionHandler.FLASH_ERROR);
if(!(flashError==null)){
String resolvedFlashError = flashError;
if (!(propertyResourceBundle==null)){
try{
resolvedFlashError = propertyResourceBundle.getString(flashError);
}catch(Exception e){}
}
model.put(SessionHandler.FLASH_ERROR, resolvedFlashError);
}


String flashMessage = sessionHandler.consumeFlash(request, SessionHandler.FLASH_MESSAGE);
if(!(flashMessage==null)){
String resolvedFlashMessage = flashMessage;
if (!(propertyResourceBundle==null)){
try{
resolvedFlashMessage = propertyResourceBundle.getString(flashMessage);
}catch(Exception e){}
}
model.put(SessionHandler.FLASH_MESSAGE, resolvedFlashMessage);
}

Set renderedAttributes = new HashSet();
renderedAttributes.add(SessionHandler.FLASH_ERROR);
renderedAttributes.add(SessionHandler.FLASH_MESSAGE);

return(new ModelAndView(view, model));

}
Couple of things to note here. Since I've added support for key-value from a resource properties file, if the flash message sent is found in the resource file (classpath:ApplicationResources.properties by default), it uses that, or else it just sends the message sent.

Now the question is, we have a way and place to set the flash error/message and now a way to retrieve the message. How do we show it on a view. That comes next.

4. Invoke FlashController from your view using ajax call.

<script>
$.getJSON("${ctx}/flash.html",
function(data){
$.each(data, function(i,item){
var curDiv = (i=='FLASH_ERROR')?"#flashError":"#flashMessage";
$(curDiv).html(item).fadeIn('slow');
});
});
</script>
<div id="flashError" style="display:none;"></div&gt;
<div id="flashMessage" style="display:none;"></div>

That's it. using jQuery getJSON method, we get the messages, and if received show them.

I'm sure there are limitations and constraints in this method, such as using javascript, ajax may be a constraint. I'm sure workarounds can be found. And if not, then using Spring's custom flash scope is always an option. I would like to hear from you if you find any security issue in this solution. n'joy.

Additional reading:
Custom Scope for Flash Scope discussion
Spring by example Custom ThreadScope

2 comments: