mvc

Spring web wizard with annotations and clean URLs

Spring MVC documentations declares the AbstractWizardFormController class as obsolete in favor of annotations. Which are really awesome: they eliminate XML jungles in Java webdevelopmentland.

However there is no meta-annotation of @Controller for wizards. (A wizard is basically a multipage view/form.)
We have to track the current page and handle page switches and final submission properly. I try to simplify logic by the expressiveness of annotations.

All the context configuration for Spring:

        <context:annotation-config/>
        <context:component-scan base-package="the.wizardcontrollers.package" />
 
        <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
        <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
 
        <bean id="viewResolver"
          class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp" />

Our controller:

@Controller
@RequestMapping("/wizard")
public class WizardController {
 
        static final String[] pages = {"First", "Second", "Last"};
 
        @RequestMapping
        public ModelAndView submittedView(@ModelAttribute("loss") Loss loss,
                  @RequestParam(value = "nextPage", required = false) Integer page,
                  @RequestParam(value = "next", required = false) Object next,
                  @RequestParam(value = "prev", required = false) Object prev,
                  @RequestParam(value = "curPage", required=false) Integer curPage) {
 
                if (curPage!=null&&next!=null) page=curPage+1;
 
                if (curPage!=null&&prev!=null) page=curPage-1;
 
                if (page==null||page<1||page>pages.length) page = 1;
 
                ModelAndView model = new ModelAndView("wizard");
 
                model.addObject("pageNum", page).addObject("pageMax", pages.length).addObject("pageView", pages[page - 1]);
 
/* IMPLEMENT YOUR LOGIC */
 
                return model;
        }
 
        @RequestMapping(params = "submit")
        public View committedView(SessionStatus status) {
 
/* VALIDATE in failure do not redirect or will loose 
conversation session attributes, i.e. your current model
Insted return a call to the above submitView()*/
 
/* IMPLEMENT YOUR LOGIC in transaction with the following call*/
                status.setComplete();
 
                return new RedirectView("/whatever", true);
        }
}

It's a basic controller, mapping to the requests to /wizard. The pages array contains the metadata of the individual pages, in our case simple strings uniquely identifying each one. We do not implement a state-machine or keep track of the current page in the session. The current page is requested explicitly in the request (could be also a path variable). If omitted or invalid defaulting to page number 1. It returns the view "wizard".

In /WEB-INF/jsp/wizard.jsp we will have access to the attributes pageNum (the current page number counting from 1), pageMax (the total number of pages) and pageView (exposing the unique name of the page). It can request a page explicitly with pageNext in the request or request the previous or successive page by including next or prev in the request. In the latter case the current page number (to increment or decrement) must be in curPage. Exclude the options you don't want to allow the user to take. Including submit in the request signals completion of the conversation.

By using a redirect when committing, we do a clean PRG.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Wizard</title>
    </head>
    <body>
        <h1>Wizard, Yeah</h1>
        Step <c:out value="${pageNum}"/>/<c:out value="${pageMax}"/>.
        <form:form commandName="wizard">
 
                        <jsp:include page="wizard${pageView}.jsp"/>
 
                        <input name="curPage" type="hidden" value="${pageNum}"/>
                        <c:if test="${pageNum > 1}">
                                <input name="prev" type="submit" value="Back" />
                        </c:if>
                        <c:if test="${pageNum < pageMax}">
                                <input name="next" type="submit" value="Next" />
                        </c:if>
                        <c:if test="${pageNum == pageMax}">
                                <input name="submit" type="submit" value="Finish" />
                        </c:if>
        </form:form>
    </body>
</html>

You can add nextPage to the request to request a specific page (for example in links to them). Using POST means your URLs stay completely clean, but including nextPage in the URL is also possible, also by a pathVariable. You can choose to implement individual views as pages for the wizard, for this return a view name according to the page number in the controller. But usually you do not want to duplicate the page and form layout over those. Instead the controller always returns the wizard view, which in turn includes the current page by . This does not generate a new request, instead forwards the request within the current one on java code level, i.e. the subview inherits all properties of the current request. This is much simpler and cleaner than using Tiles2. Create /WEB-INF/jsp/wizard[First|Second|Last].jsp for example as:

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:out value="${pageView}"/>

Take care of validating the model before committing it! You can opt to validate each page upon page switch or to allow only linear navigation.

Syndicate content