4. Simple Spring MVC Web Module

The web module has a simple form for creating and editing a Person, and also has a basic search page. The application also has internationalization and uses Tiles for templating.

Manifest Configuration

src/main/resources/META-INF/MANIFEST.MF
                    
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Simple Spring MVC Web Module
Bundle-Description: Simple Spring MVC Web Module
Bundle-SymbolicName: org.springbyexample.sdms.simpleForm.webModule  1
Bundle-Version: 1.0.0  2
Bundle-Vendor: Spring by Example
Module-Type: Web  3
Web-ContextPath: simple-form  4
Web-DispatcherServletUrlPatterns: *.html  5
Import-Bundle: com.springsource.org.apache.taglibs.standard,
 com.springsource.javax.servlet
Import-Library: org.aspectj;version="[1.6.0,1.7.0)",
 org.springframework.spring;version="[2.5.5,3.0.0)",
 org.hibernate.ejb;version="[3.3.2.GA,3.3.2.GA]",
 org.apache.tiles;version="[2.0.5.osgi,2.0.5.osgi]"
Import-Package: org.springbyexample.sdms.simpleForm.orm.bean;version="[1.0.0,1.1.0]",
 org.springbyexample.sdms.simpleForm.orm.dao;version="[1.0.0,1.1.0]"  6
                    
                
1 The symbolic name the bundle is deployed under.
2 The version the bundle is deployed under.
3 Declares the module type indicating this is a web module. This is used by the Spring dm Server.
4 The Web-ContextPath property configures the web application's context path.
5 The Web-DispatcherServletUrlPatterns property configures the dispatch servlet to route any matches for the pattern '*.html' to controllers.
6 The Import-Package property imports the the persistence beans and the Person DAO interface from the Person DAO bundle.

Spring Configuration

The bundle-context.xml isn't used in this example, but bundle-context-osgi.xml makes a reference to the Person service and exposes it as a bean. The Spring web configuration is in webmvc-context.xml.

The reference to the OSGi service PersonDao is exposed as a bean named personDao.

src/main/resources/META-INF/spring/bundle-context-osgi.xml
                    
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/osgi"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/osgi 
                           http://www.springframework.org/schema/osgi/spring-osgi.xsd">

    <reference id="personDao" interface="org.springbyexample.sdms.simpleForm.orm.dao.PersonDao"/>
  
</beans:beans>
                    
                

This standard Spring MVC configuration file scans for controller classes, creates handlers, configures Tiles, and also internationalization. The classnameControllerMappings bean enables convention based mappings for reduced configuration. It is configured to be case sensitive, so a controller called StudentPersonController would map to the URL '/studentPerson'. Although it defaults to case insensitive and would then map to the URL '/studentperson'. The default handler is set with the UrlFilenameViewController, which will handle any requests not handled by a convention based controller.

src/main/resources/META-INF/spring/webmvc-context.xml
                    
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context 
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springbyexample.sdms.simpleForm.web.mvc" />
    
    <!-- Enables /[resource]/[action] to [Resource]Controller class mapping -->
    <bean id="classnameControllerMappings" class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"
          p:order="1"
          p:interceptors-ref="localeChangeInterceptor" 
          p:caseSensitive="true">
        <property name="defaultHandler">
            <bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
        </property>
    </bean>
    
    <!-- Enables annotated POJO @Controllers -->
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
    
    <!-- Enables plain Controllers -->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
    
    <bean id="tilesConfigurer"
    	  class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
    	<property name="definitions">
    		<list>
    			<value>/WEB-INF/tiles-defs/templates.xml</value>
    		</list>
    	</property>
    </bean>
    
    <bean id="tilesViewResolver"
    	  class="org.springframework.web.servlet.view.UrlBasedViewResolver"
          p:order="2"
    	  p:viewClass="org.springframework.web.servlet.view.tiles2.TilesView" />
    
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
          p:basenames="messages" />
    
    <!-- Declare the Interceptor -->
    <bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"
          p:paramName="locale" />
    
    <!-- Declare the Resolver -->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />

</beans>
                    
                

JSP Example

A simple person form using Spring's custom JSP tags.

src/main/resources/MODULE-INF/WEB-INF/jsp/person/form.jsp
                    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<h1><fmt:message key="person.form.title"/></h1>

<c:if test="${not empty statusMessageKey}">
    <p><fmt:message key="${statusMessageKey}"/></p>
</c:if>

<c:url var="url" value="/person/form.html" /> 
<form:form action="${url}" commandName="person">
    <form:hidden path="id" />

    <fieldset>
        <div class="form-row">
            <label for="firstName"><fmt:message key="person.form.firstName"/>:</label>
            <span class="input"><form:input path="firstName" /></span>
        </div>       
        <div class="form-row">
            <label for="lastName"><fmt:message key="person.form.lastName"/>:</label>
            <span class="input"><form:input path="lastName" /></span>
        </div>
        <div class="form-buttons">
            <div class="button"><input name="submit" type="submit" value="<fmt:message key="button.save"/>" /></div>
        </div>
    </fieldset>
</form:form>
                    
                

This is the search page and next to each record displayed is an 'edit' and 'delete' link.

src/main/resources/MODULE-INF/WEB-INF/jsp/person/search.jsp
                    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<h1><fmt:message key="person.search.title"/></h1>

<table class="search">
    <tr>
        <th><fmt:message key="person.form.firstName"/></th>
        <th><fmt:message key="person.form.lastName"/></th>
    </tr>
<c:forEach var="person" items="${persons}" varStatus="status">
    <tr>
        <c:set var="personFormId" value="person${status.index}"/>

        <c:url var="editUrl" value="/person/form.html">
            <c:param name="id" value="${person.id}" />
        </c:url>
        <c:url var="deleteUrl" value="/person/delete.html"/>
        <form id="${personFormId}" action="${deleteUrl}" method="POST">
            <input id="id" name="id" type="hidden" value="${person.id}"/>
        </form>

        <td>${person.firstName}</td>
        <td>${person.lastName}</td> 
        <td>
            <a href='<c:out value="${editUrl}"/>'><fmt:message key="button.edit"/></a>
            <a href="javascript:document.forms['${personFormId}'].submit();"><fmt:message key="button.delete"/></a> 
        </td>
    </tr>
</c:forEach>
</table>
                    
                

Below is an example of an alternative way to handle a delete following a REST style approach, although the following JavaScript won't work in IE. A library like Dojo should be used for the AJAX call to make the JavaScript browser independent.

The delete method in the controller would be made to only accept an HTTP DELETE. A standard HTML form can't submit this type of request, but JavaScript's XMLHttpRequest can. Ideally in a more complex example, Spring JS would be used to decorate the link with JavaScript and there would be a URL that would still function with a standard GET or POST in case the user has JavaScript disabled in their browser.

The delete link calls the JavaScript function deletePerson passing in the URL that should be called by XMLHttpRequest using an HTTP DELETE (instead of the usual GET or POST). Before the delete is processed it is confirmed by a JavaScript popup.

Alternative search JSP
                    
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<script language="javascript">
//<!--        
function deletePerson(url){
    var confirmed = confirm('<fmt:message key="person.form.confirmDelete"/>');

    if (confirmed) {
        var request = new XMLHttpRequest();
        request.open('DELETE', url, true);
        request.onreadystatechange = function () { window.location.reload(true); };
        request.send(null);
    }
}
// -->
</script>

<h1><fmt:message key="person.search.title"/></h1>

<table class="search">
    <tr>
        <th><fmt:message key="person.form.firstName"/></th>
        <th><fmt:message key="person.form.lastName"/></th>
    </tr>
<c:forEach var="person" items="${persons}">
    <tr>
        <c:url var="editUrl" value="/person/form.html">
            <c:param name="id" value="${person.id}" />
        </c:url>
        <c:url var="deleteUrl" value="/person/delete.html">
            <c:param name="id" value="${person.id}" />
        </c:url>

    	<td>${person.firstName}</td>
        <td>${person.lastName}</td> 
    	<td>
            <a href='<c:out value="${editUrl}"/>'><fmt:message key="button.edit"/></a>
            <a href="javascript:deletePerson('<c:out value="${deleteUrl}"/>')"><fmt:message key="button.delete"/></a> 
        </td>
    </tr>
</c:forEach>
</table>
                    
                

Code Example

Because of the classnameControllerMappings bean, the controller is automatically mapped to handle '/person/*' based on it's name (ex: [Path]Controller). Then the method names are used to match the rest of the path. So, '/person/form' will match the form method. The @ModelAttribute will be called before each path mapped to the controller. For example, when '/person/form' is called, the newRequest method will be called first so an instance of Person is available to bind to the form.

The search method doesn't currently use the person model attribute from the newRequest method, but in a more advanced example it likely would. For example using the first and last name fields to perform a like search in the database.

Example 3. Simple Spring MVC PersonController

sdms/simple-spring-mvc/simple-spring-mvc-web-module/src/main/java/org/springbyexample/sdms/simpleForm/web/mvc/PersonController.java
                    
@Controller
public class PersonController {

    private static final String SEARCH_VIEW_KEY = "redirect:search.html";
    private static final String SEARCH_MODEL_KEY = "persons";

    @Autowired
    protected PersonDao personDao = null;

    /**
     * For every request for this controller, this will 
     * create a person instance for the form.
     */
    @ModelAttribute
    public Person newRequest(@RequestParam(required=false) Integer id) {
        return (id != null ? personDao.findPersonById(id) : new Person());
    }

    /**
     * <p>Person form request.</p>
     * 
     * <p>Expected HTTP GET and request '/person/form'.</p>
     */
    @RequestMapping(method=RequestMethod.GET)
    public void form() {}

    /**
     * <p>Saves a person.</p>
     * 
     * <p>Expected HTTP POST and request '/person/form'.</p>
     */
    @RequestMapping(method=RequestMethod.POST)
    public void form(Person person, Model model) {
        if (person.getCreated() == null) {
            person.setCreated(new Date());
        }

        Person result = personDao.save(person);
        
        // set id from create
        if (person.getId() == null) {
            person.setId(result.getId());
        }

        model.addAttribute("statusMessageKey", "person.form.msg.success");
    }
    
    /**
     * <p>Deletes a person.</p>
     * 
     * <p>Expected HTTP POST and request '/person/delete'.</p>
     */
    @RequestMapping(method=RequestMethod.POST)
    public String delete(Person person) {
        personDao.delete(person);
        
        return SEARCH_VIEW_KEY;
    }

    /**
     * <p>Searches for all persons and returns them in a 
     * <code>Collection</code>.</p>
     * 
     * <p>Expected HTTP GET and request '/person/search'.</p>
     */
    @RequestMapping(method=RequestMethod.GET)
    public @ModelAttribute(SEARCH_MODEL_KEY) Collection<Person> search() {
        return personDao.findPersons();
    }

}