Richard’s Weblog

January 30, 2009

Spring FrameWork, testing and coding basic form submission using AbstractFormController.

Filed under: Spring Framework,Web development — Richard @ 6:00 pm
Tags: ,

This example will use the same scenario that is used in a previous post about using the Controller interface.  I want a page that allows the user to search for books.  The user should first see a search form, with a text box and a search button.  When clicking on the button, a search is done and the results are displayed on this same web page. While filling the form, the user can specify if he wants to display 3, 5 or 10 results at the same time on the page (the page size). Navigation among results will be possible using “Next” and “Previous” buttons, starting at page 1. 

To develop the example, I need a controller, a view, a “backing object” (or “command object”) and of course, a unit test for the controller. The controller will be an AbstractFormController, and the view will be written using only JSP/JSTL.

The Backing Object (a.k.a. the command object)

I’ll use a SearchCriteria object to exchange information between the controller and the view, such as the submitted search string, page number, page size, and search results.

public class SearchCriteria implements Serializable {
	private static final long serialVersionUID = 1L;

    private String searchString;
    private int page= 1;
    private int pageSize= 3;
    private Collection<Book> results;
    private int numberOfResults = 0;

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public Collection<Book> getResults() {
        return results;
    }

    public void setResults(Collection<Book> results) {
        this.results = results;
    }

    public int getNumberOfResults() {
        return numberOfResults;
    }

    public void setNumberOfResults(int numberOfResults) {
        this.numberOfResults = numberOfResults;
    }

    public String getSearchString() {
    	return searchString;
    }

    public void setSearchString(String searchString) {
    	this.searchString = searchString;
    }
}

Spring must take care of building a SearchCriteria object from the HttpServletRequest. It is basically what AbstractFormController is made for. To obtain a SearchCriteria object from a posted form, I normally would have to write code like this :

SearchCriteria criteria = new SearchCriteria();
criteria.setSearchString(request.getParameter("searchString"));
criteria.setPage(Integer.parseInt(request.getParameter("page")));
criteria.setPageSize(Integer.parseInt(request.getParameter("pageSize")));

But AbstractController can do this job for me. For that, I must use setCommandClass(Class) method that will tell my controller what type of object to use for creating the backing object. Once the command class set, AbstractController will be able to do the binding and to provide me with a SearchCriteria object upon form submission. After performing a search, the controller pushes the same SearchCriteria, populated with the search results to display, in the model for it to be passed to the view.

This SearchCriteria object will also be used in my unit test.

The Test

I describe this scenario’s controller workflow as this : the user sends a “GET” request to the controller. The controller reacts by sending the view that displays the search form. The user then enter a search string, and the number of books that will be displayed on a single page. He then submits the form. A “POST” request is sent to the controller, telling it to trigger the search. The returned list of books is hard-coded for now. The controller then calls the view that will display the results (for this example, the same view will be used to display the form and to display the results).

Testing the controller is rather straightforward. I use spring-test mocks for mocking a request and a response, and then call the controller’s handleRequest(httpServletRequest, HttpServletResponse) method. Then I just grab the SearchCriteria object from the returned model, and assert it was correctly populated by the controller :

public class SearchControllerTest {
	private SearchController controller= new SearchController();

	@Test
	public void testGetForm() {
		MockHttpServletRequest request = new MockHttpServletRequest();
		MockHttpServletResponse response = new MockHttpServletResponse();
		controller.setCommandClass(SearchCriteria.class);

		request.setMethod("GET");
		ModelAndView mav = null;
		try {
			 mav = controller.handleRequest(request, response);
		} catch(Exception e) {
			e.printStackTrace();
			fail();
		}
		assertEquals("/book/searchForm.jsp", mav.getViewName());
	}

	@Test
	public void testSubmitSearch() {
		MockHttpServletRequest request = new MockHttpServletRequest();
		request.setMethod("POST");
		String searchString = "Test-Driven Development";
		request.setParameter("searchString", searchString);
		int pageSize = 5;
		request.setParameter("pageSize", String.valueOf(pageSize));
		int page = 2;
		request.setParameter("page", String.valueOf(page));
		MockHttpServletResponse response = new MockHttpServletResponse();
		controller.setCommandClass(SearchCriteria.class);
		ModelAndView mav = null;
		try {
			 mav = controller.handleRequest(request, response);
		} catch(Exception e) {
			e.printStackTrace();
			fail();
		}
		SearchCriteria searchCriteria = (SearchCriteria)mav.getModel().get("criteria");
		assertNotNull(searchCriteria);
		assertEquals(searchString, searchCriteria.getSearchString());
		assertEquals(pageSize, searchCriteria.getPageSize());
		assertEquals(page, searchCriteria.getPage());
		assertEquals("/book/searchForm.jsp", mav.getViewName());
	}

}

Note the use of the setCommandClass method, to tell AbstractController our backing object representing the form will be of type SearchCriteria. In production, this will be configured by the IOC :

<bean name="/book/search" class="com.company.x.samples.bookbay.search.SearchController">
<property name="commandClass" value="com. company.x.samples.bookbay.search.SearchCriteria"/>
</bean>

The Controller

When working with AbstractFormController, basically we must override two methods. The first one is named “showForm”, and is called when a “GET” http request comes in. This method is signed with the usual HttpServletRequest and HttpServletResponse parameters, plus another object of type BindException (it could be used for some validation purpose, but I won’t use it in my example) :

protected abstract ModelAndView showForm(
			HttpServletRequest request, HttpServletResponse response, BindException errors)
			throws Exception;

The other method to override is the one that will be called to process form submission. This method is cleverly named “processFormSubmission”. It receives an HttpServletRequest, an HttpServletResponse, the backing object (here my SearchCriteria object) and again a BindException for which I have no need :

protected abstract ModelAndView processFormSubmission(
			HttpServletRequest request, HttpServletResponse response, Object command, BindException errors)
			throws Exception;

Now it’s time to code the controller, for it can react as we specified in our unit test :

public class SearchController extends AbstractFormController {

    @Override
    protected ModelAndView showForm(HttpServletRequest request,
            HttpServletResponse response, BindException errors)
            throws Exception {
        return showForm(request, errors, "/book/searchForm.jsp");
    }

    @Override
	public ModelAndView processFormSubmission(HttpServletRequest request, HttpServletResponse response, Object criteria, BindException errors) throws Exception {
			getBooks((SearchCriteria) criteria);
			ModelAndView mav = new ModelAndView("/book/searchForm.jsp");
			mav.addObject("criteria", criteria);
            return mav;
	}

	/**
	 * Find books according to a search string, and return the fetched element, taking care of pagination.
	 * @param searchString Search criteria.
	 * @param page The page to fetch. 1 is the first page.
	 * @param pageSize Maximum number of books per page.
	 * @param results List of books to display on the specified page, according to the pageSize.
	 * @return The total number of books that would have been returned if pageSize was ignored.
	 */
	private void getBooks(SearchCriteria criteria) {
		List<Book> books = new LinkedList<Book>();
		// Fake we found some results.
		Book book = new Book();
		book.setTitle("Test Driven Development");
		book.setIsbn("0-321-14653-0");
		book.setAuthor("Kent Beck");
		books.add(book);

		book = new Book();
		book.setTitle("Design Patterns, Elements of Reusable Object-Oriented Software");
		book.setIsbn("0-201-63361-2");
		book.setAuthor("Erich Gama, Richard Helm, Ralph Johnson, John Vlissides");
		books.add(book);

		book = new Book();
		book.setTitle("Enterprise Integration Patterns");
		book.setIsbn("0-321-20068-3");
		book.setAuthor("Gregor Hohpe, Bobby Woolf");
		books.add(book);

		book = new Book();
		book.setTitle("Refactoring to Patterns");
		book.setIsbn("0-321-21335-1");
		book.setAuthor("Joshua Kerievsky");
		books.add(book);

		book = new Book();
		book.setTitle("The Timeless Way of Building");
		book.setIsbn("0-19-502402-8");
		book.setAuthor("Christopher Alexander");
		books.add(book);

		book = new Book();
		book.setTitle("Peopleware, 2nd edition");
		book.setIsbn("0-932633-43-9");
		book.setAuthor("Tom DeMarco, Timothy Lister");
		books.add(book);

		book = new Book();
		book.setTitle("Extreme Programming Explained");
		book.setIsbn("0-321-27865-8");
		book.setAuthor("Kent Beck, Cynthia Andres");
		books.add(book);

		if(criteria.getPage() < 1)
            criteria.setPage(1);

        int startIndex = criteria.getPageSize()*(criteria.getPage()-1);
        int endIndex = criteria.getPage()*criteria.getPageSize();
        if(endIndex > books.size())
            endIndex = books.size();
		criteria.setResults(books.subList(startIndex, endIndex));
		criteria.setNumberOfResults(books.size());
	}
}

The “showForm” method just forwards to another AbstractFormController method of the same name, but taking the name of the “form view” we must return as a parameter. In this example I could have built and return a ModelAndView manually.

The “processFormSubmission” method takes the SearchCriteria object AbstractFormController, prepared for us when receiving the request, and passes it to the getBooks method which will perform the search and populate the SearchCriteria object with the results. My “processFormSubmission” method then attaches the modified SearchCriteria object to the model, for the view to be able to access it. Actually, the “getBooks” method would be in another object, probably called something like BookService, and backed by a persistence mecanism. But this is just an example :)

The View

Now I need a view to render the form and the results. I’ll do it simply in JSP, using JSTL to access the model :

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
    <body>
        <c:url var="searchControllerURL" value="/book/search"/>
        <form method="POST" action="${searchControllerURL}">
            <input type="text" name="searchString" value="${criteria.searchString}"/>
            <input type="submit" value="search"/>
            Display
            <select name="pageSize">
                <option value="3" <c:if test="${criteria.pageSize eq 3}">selected="selected"</c:if>>3</option>
                <option value="5" <c:if test="${criteria.pageSize eq 5}">selected="selected"</c:if>>5</option>
                <option value="10"<c:if test="${criteria.pageSize eq 10}">selected="selected"</c:if> >10</option>
            </select>
             results.
        </form>

        <c:if test="${not empty criteria.results}">
            Search results for "${criteria.searchString}" :

<table border="1">
                <c:forEach var="book" items="${criteria.results}" varStatus="rowCounter">
                    <c:if test="${rowCounter.count eq 1}">
<tr>
<th>Title</th>
<th>Author</th>
<th>ISBN</th>
</tr>
</c:if>
<tr>
<td>${book.title}</td>
<td>${book.author}</td>
<td>${book.isbn}</td>
</tr>
</c:forEach></table>
<c:if test="${criteria.page > 1}">
                <form style="display: inline;" method="POST" action="${searchControllerURL}">
                    <input type="hidden" name="searchString" value="${criteria.searchString}"/>
                    <input type="hidden" name="page" value="${criteria.page-1}"/>
                    <input type="hidden" name="pageSize" value="${criteria.pageSize}"/>
                    <input type="submit" name="previous" value="previous"/>
                </form>
            </c:if>
            <c:if test="${criteria.page*criteria.pageSize < criteria.numberOfResults}">
                <form style="display: inline;" method="POST" action="${searchControllerURL}">
                    <input type="hidden" name="searchString" value="${criteria.searchString}"/>
                    <input type="hidden" name="page" value="${criteria.page+1}"/>
                    <input type="hidden" name="pageSize" value="${criteria.pageSize}"/>
                    <input type="submit" name="next" value="next"/>
                </form>
            </c:if>
        </c:if>
    </body>
</html>

Here, I could have use the Spring tags to bind the form to the backing object, but my intention was to focus on the controller and the test. So maybe in another post I’ll talk about the way Spring can help us writing the view.

Advertisements

7 Comments »

  1. that was quite informative …

    Comment by suresh — April 20, 2009 @ 11:53 am | Reply

  2. How about use the Spring 2.5 MVC instead of Spring MVC 1.x approach? You will be delight.

    Comment by vicina.info — July 30, 2009 @ 7:22 pm | Reply

  3. Thanks for the blogpost.

    Comment by Irrebramayors — January 2, 2010 @ 2:11 am | Reply

  4. A very well-organised blog and very informative. I have just book marked this blog and will want to develop a developer relationship with you. I’m a software developer trying to get his ground in Java.

    Please keep this blog running.

    Comment by Bobby — March 9, 2010 @ 6:16 am | Reply

  5. fantastic!

    Comment by Eugene — March 16, 2010 @ 6:27 pm | Reply

  6. Hi,
    Thank u very much for providing me that kind of clear explanation. This have given the best insight into that Controller.

    Comment by guru — December 2, 2010 @ 9:18 am | Reply

  7. Excellent

    Comment by Anonymous — May 18, 2011 @ 5:25 pm | Reply


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: