Search This Blog

Sunday, 2 February 2014

Views in ModelAndView

We had an interesting scenario in our code. It all started out when we had this complex controller that included methods specific to a certain flow. In case of any exception, we used an error handler to redirect the flow to an error page. The code would be along the lines of
@Controller
public class SimpleController {

   // various view methods
   @RequestMapping(value = "/home.do", method = RequestMethod.GET)
   public ModelAndView display() {
      // complex logic executed here
      // in case of error
      throw new RuntimeException("There was a failure in Page call");
   }

   @ExceptionHandler(Exception.class)
   public ModelAndView resolveException(final HttpServletRequest request,
         final HttpServletResponse response, final Exception ex) {
      final ModelAndView mv = new ModelAndView("error/500");
      // add details of error to the model
      return mv;
   }

}
As seen in case of any failure, the flow was redirected to the ExceptionHandler method. The code simply built the error objects and redirected to the error page. All was fine. Until we got a change. One of the flows had to be changed to an AJAX method.
This was simple. With Spring's fantastic annotations we simply had to remove the ModelAndView
and introduce the @ResponseBody annotation. Pretty cool.
@RequestMapping(method = RequestMethod.GET, value = "/call")
   public @ResponseBody
   Object getResource() {
      // complex logic...
      // build the object

      // if failure handle the exception
      throw new RuntimeException("There was a failure in Service call");
      // return the object
   }
The flow was integrated and all worked fine. Until one fine day. OK thats a lie!! Until 5 minutes later. The UI dude asked "What happens when the server fails ?"
We could not redirect to the JSP page in the middle of an AJAX. In fact we were not to touch the error JSP nor could we to move the AJAX logic. (Don't ask why.)
So we had to change our resolveException method to generate two different views -
  1. A JSP when our controller methods failed and 
  2. a JSON when our AJAX call failed. 
We were stumped... until we googled the problem (of course). Turns out the ModelAndView is not just a simplified way to add attributes and specify views. It is much more powerful. Our new code was now built to handle both cases:
@ExceptionHandler(Exception.class)
   public ModelAndView resolveException(final HttpServletRequest request,
         final HttpServletResponse response, final Exception ex) {

      final ModelAndView mv = new ModelAndView();
      final String header = request.getHeader("X-Requested-With");

      if ("XMLHttpRequest".equalsIgnoreCase(header)) { // an AJAX request
         response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView();
         mappingJacksonJsonView.setExtractValueFromSingleKeyModel(true);
         mv.setView(mappingJacksonJsonView);

         // the details of the error
         final Error error = new Error();
         error.setMsg("A 500 error was encountered !");
         mv.addObject(error);
      } else { // a JSP style error
         mv.setViewName("error/500");
      }
      return mv;
   }
As seen the View need not simply have to be a string. (In reality the string too resolves to an instance of InternalResourceView). The View could be a MappingJacksonJsonView which ensures that the response is written back as JSON.
The Error class is a simple POJO:
class Error {
   private String msg;
   //setter getters
}
If I were to hit the URL "http://<application>/home.do" the the logs indicate:
ExceptionHandlerExceptionResolver:132 - Resolving exception from handler [public org... ModelAndView 
com..SimpleController.display()]:java.lang.RuntimeException: There was a failure in Page call
ExceptionHandlerExceptionResolver:319 - Invoking @ExceptionHandler method: public org...ModelAndView
com...SimpleController.resolveException(javax...HttpServletRequest,javax...HttpServletResponse,
java.lang.Exception)
HandlerMethod:129 - Invoking [resolveException] method with arguments [
org.apache.catalina.connector.RequestFacade@109b5ca, org.apache.catalina.connector.ResponseFacade@9bcf06,
 java.lang.RuntimeException: There was a failure in Page call]
HandlerMethod:135 - Method [resolveException] returned [ModelAndView: reference to view with name
 'error/500'; model is null]
DispatcherServlet:1206 - Rendering view [org.springframework.web.servlet.view.InternalResourceView:
 name 'error/500'; URL [/jsp/error/500.jsp]] in DispatcherServlet with name 'DispatcherServlet'
To test the AJAX flow involved a little more code. I created a simple HTML page for the same:
<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript">
      function run() {
      var xmlhttp = new XMLHttpRequest();
     xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState == 4 ) {
            document.getElementById("myDiv").innerHTML = xmlhttp.responseText;
      }
      };
       xmlhttp.open("GET", "api/call", true);
       xmlhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest");
       xmlhttp.send();
       }
    </script>
  </head>
  <body>
    <h2>Testing the AJAX call</h2>
    <button type="button" onclick="run()">Make Call</button>
    <div id="myDiv"></div>
  </body>
</html>
The code simply makes an AJAX call. The logs indicate the failure:
Resolving exception from handler [public java.lang.Object com...SimpleController.getResource()]: 
java.lang.RuntimeException: There was a failure in Service call
DEBUG ExceptionHandlerExceptionResolver:319 - Invoking @ExceptionHandler 
method: public org....ModelAndView com....SimpleController.resolveException(javax...HttpServletRequest,
javax...HttpServletResponse,java.lang.Exception)
DEBUG HandlerMethod:129 - Invoking [resolveException] method with arguments [
org.apache.catalina.connector.RequestFacade@109b5ca, org.apache.catalina.connector.ResponseFacade@9bcf06,
java.lang.RuntimeException: There was a failure in Service call]
DEBUG HandlerMethod:135 - Method [resolveException] returned [ModelAndView: materialized View is
[org.springframework.web.servlet.view.json.MappingJacksonJsonView: unnamed]; model is {error=
com.controller.SimpleController$Error@27d489}]
...
DispatcherServlet:1206 - Rendering view [org.springframework.web.servlet.view.json.MappingJacksonJsonView:
 unnamed] in DispatcherServlet with name 'DispatcherServlet'
As seen the View object in ModelandView can be replaced with any custom view that suits our application. If we simply look in the spring packages we can find a large number of views indicating support for Velocity, Jasper, PDF, Excel and so many more..

1 comment: