Sunday, August 12, 2012

Using Spring Form Binding When the View Resolver Doesn't Support It

Spring form binding is a convenient way to get valid data objects from a user in Spring MVC. If you need to use a View technology other than JSP, however, things may not just work, so here's some information that may fill in the gaps.

The situation

The particular situation I encountered involves using Freemarker for a template language.  Above and beyond just Freemarker I'm also using Spring Surf, so the standard solution (covered below) doesn't apply.  This post covers a direct usable solution that works in a technology agnostic way (aside from Spring) and should at least provide information for other solutions.  

Standard solution and what's going on

The standard means of setting up Freemarker (and other View technologies) for Spring form binding is to add some settings to the view resolver configuration.  Something along the lines of:

<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
   <property name="exposeSpringMacroHelpers"><value>true</value></property>
   <property name="exposeRequestAttributes"><value>true</value></property>
   <property name="exposeSessionAttributes"><value>true</value></property>
</bean>

where the first property does the needed set-up for form binding (the other two merge request and session info into the model).  Tracking this down through the source code this setting is ultimately passed from the view resolver to the AbstractTemplateView (source) which adds a RequestContext to the model (and does the request & session merging).

Re-using that behavior

Unfortunately this is normally re-used through inheritance, and in the case of Spring Surf neither the relevant view resolver nor the AbstractTemplateView class are in the used hierarchy.  It could also be argued that this functionality shouldn't really be handled by the view resolver at all since it is more of an application concern, though I'd see both sides of the possible argument having pretty even weight.  I'd certainly argue that one way or the other it should be made more modular: for speed I resorted to the cut and paste route.

A sensible place to get the Model set up as needed for the View to hook in to it would be right in between when the Controller is done doing it's work and before the View resolver does its resolving.  In Spring this can be done with the postHandle hook of a HandlerInterceptor.  For consistency I've borrowed the same properties/flags as the AbstractTemplateView.  An additional caveat due to being moved before the View resolver is that every request will be intercepted, even those that aren't relevant and possibly don't have a Model such as those handled by a MessageConverter.  An additional null check takes care of that.

A sample interceptor would then be something like (season to taste):

public class RequestContextInterceptor extends HandlerInterceptorAdapter implements ApplicationContextAware {
  private ApplicationContext applicationContext;

  private boolean exposeSpringMacroHelpers = true;
  private boolean exposeRequestAttributes = true;
  private boolean exposeSessionAttribute = true;

  public static final String MODEL_KEY = "springMacroRequestContext";

  public void setExposeSpringMacroHelpers(boolean exposeSpringMacroHelpers) {
    this.exposeSpringMacroHelpers = exposeSpringMacroHelpers;
  }
//...Other setters

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

  //When using mesage converters or other non model requests
  if (modelAndView == null) return;

  if (exposeSpringMacroHelpers) {
    if (!modelAndView.getModel().containsKey(MODEL_KEY)) {
     //Throw together a usable RequestContext...seems to require ApplicationContextAware-ness
      modelAndView.addObject(MODEL_KEY, new RequestContext(request, response,((WebApplicationContext) applicationContext).getServletContext(), modelAndView.getModel()));
    }
  }

  if (exposeRequestAttributes) {
    //...Code stolen from AbstractTemplateView
  }
  //..Session code stolen from AbstractTemplate View
}

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
}

This can then be wired in to Spring:

    <mvc:interceptors>        
        <bean class="com.example.handlerinterceptors.RequestContextInterceptor">
          <property name="exposeSpringMacroHelpers" value="true"/>
          <property name="exposeRequestAttributes" value="true"/>
        </bean>
    </mvc:interceptors>

To avoid conflicts, disable the settings in the view resolver configuration.

For Freemarker there is also a form binding library that is normally automatically exposed for use.  Rather than muck around with getting that working and also because I like to be able to easily reference the source for that file, I opted to just download and use the file as a normal Freemarker import.

And there you have it: guidelines for a usable solution that is more portable than the out-of-box offering or at least some guidance that may help lead whee you need to go.  


No comments :

Post a Comment