2011-10-30

Wizard form with Spring MVC

Today I will show you how to create a wizard form with Spring MVC. A wizard form is a multi-step form that allows users to submit information gradually. With Spring MVC it's very easy to make such a form, as well as validate already submitted data on each step and on finish.

Controller

This is the main thing. Let's take a detailed look.

The controller is annotated with the @Controller annotation. @RequestMapping specifies its path. Also, we are storing user submitted data (an instance of my custom transfer object called PersonTo) in session with the @SessionAttributes annotation.
@Controller
@RequestMapping("/personRegistration")
@SessionAttributes("personTo")
public class PersonRegistrationController {
Now, let's take a look at initialisation code. The controller expects an appropriate validator to be passed during construction time. The validator is, in our case, a Spring bean, so we can have it autowired.
Then we initialise an init binder for the custom Gender enum. That allows incoming string parameters to be converted into enum instances.
@Autowired
   public PersonRegistrationController(PersonRegistrationValidator validator) {
       super();
       this.validator = validator;
   }

   @InitBinder
   public void initBinder(WebDataBinder binder) {
       binder.registerCustomEditor(Gender.class, new GenderEditor());
   }
Next, look at the method that handles GET requests. This method resets the potentially submitted data by creating a new PersonTo object and storing it in session. It requests the registration name form to be displayed. (The forms code will soon follow)
   @RequestMapping(method = RequestMethod.GET)
   public String setupForm(Model model) {
       PersonTo personTo = new PersonTo();
       model.addAttribute(PERSON_TO, personTo);
       return REGISTRATION_NAME_FORM;
   }
And now the big thing. The method that handles POST requests which are those where the user actually submitted data.
@RequestMapping(method = RequestMethod.POST)
   public String submitForm(HttpServletRequest request,
           HttpServletResponse response,
           @ModelAttribute(PERSON_TO) PersonTo personTo, BindingResult result,
           SessionStatus status, @RequestParam("_page") int currentPage,
           Model model) {

       Map<Integer, String> pageForms = new HashMap<Integer, String>();
       pageForms.put(0, REGISTRATION_NAME_FORM);
       pageForms.put(1, REGISTRATION_GENDER_FORM);

       if (userClickedCancel(request)) {
           status.setComplete();
           return REDIRECT_TO_HOMEPAGE;
       } else if (userIsFinished(request)) {
           validator.validate(personTo, result);
           if (result.hasErrors()) {
               return pageForms.get(currentPage);
           } else {
               log.info("Registration finished for person [{}: {}].",
                       personTo.getGender(), personTo.getName());
               personTo.setRegistrationComplete(true);
               return REDIRECT_TO_SUCCESS_PAGE;
           }
       } else {
           int targetPage = WebUtils.getTargetPage(request, "_target",
                   currentPage);
           if (userClickedPrevious(currentPage, targetPage)) {
               return pageForms.get(targetPage);
           } else {
               switch (currentPage) {
               case 0:
                   validator.validateName(personTo, result);
                   break;
               }
               case 1:
                   validator.validateGender(personTo, result);
                   break;
               }


               if (result.hasErrors()) {
                   return pageForms.get(currentPage);
               } else {
                   return pageForms.get(targetPage);
               }
           }
       }
   }
  1. If the user clicked cancel, cancel the whole thing by ending the session and redirect to the home page.
  2. If the user is finished, validate the whole session stored transfer object. Redirect to success page or display errors.
  3. Otherwise, this is another step in the wizard. In that case, establish which step it is and apply correct validation. If there are errors, show the current page where Spring will display error messages. If everything is OK, go to the next page.

View

I'm using Velocity. Here's the first form where the user types their name. Look at the Next and Cancel buttons (the names are important, _target1 and _cancel) and at the hidden input with page number (zero, since this is the first page in the wizard).
       <form method="post" modelAttribute="personTo">
           <table>
               <tr>
                   <td>Your name:</td>
                   <td>#springFormInput("personTo.name" "")
                   </td>
                   <td>#springShowErrors("" "")
                   </td>
               </tr>
               <tr>
                   <td colspan="3">
                       <input type="submit" value="Next" name="_target1">
                       <input type="submit" value="Cancel" name="_cancel">
                       <input type="hidden" value="0" name="_page">
                   </td>
               </tr>
           </table>
       </form>
And the gender selection page. Note we are using a drop down list here.
       <form method="post" modelAttribute="personTo">
           <table>
               <tr>
                   <td>Your gender:</td>
                   <td>#springFormSingleSelect("personTo.gender" $genders "")
                   </td>
                   <td>#springShowErrors("" "")
                   </td>
               </tr>
               <tr>
                   <td colspan="3">
                       <input type="submit" value="Previous" name="_target0">
                       <input type="submit" value="Finish" name="_finish">
                       <input type="submit" value="Cancel" name="_cancel">
                       <input type="hidden" value="1" name="_page">
                   </td>
               </tr>
           </table>
       </form>

Property editor

Here's the GenderEditor, if you're interested.
public class GenderEditor extends PropertyEditorSupport {

   @Override
   public String getAsText() {
       if (getValue() == null) {
           return null;
       } else {
           Gender gender = (Gender) getValue();
           return gender.toString();
       }
   }

   @Override
   public void setAsText(String text) throws IllegalArgumentException {
       if (StringUtils.isEmpty(text)) {
           setValue("");
       } else if (text.equalsIgnoreCase("m")) {
           setValue(Gender.MALE);
       } else if (text.equalsIgnoreCase("f")) {
           setValue(Gender.FEMALE);
       } else if (text.equalsIgnoreCase("o")) {
           setValue(Gender.OTHER);
       }
   }
}
We need the getAsText() method for validation purposes. That makes sure that if an unknown value is passed from the view, the resulting value is null (as opposed to the String "null").

Download the source code

The source code for this is available on GitHub.

2011-10-23

JEE6: Integration testing with JBoss 7

Introduction

When your code utilises some of the JEE6 goodies, such as CDI or EJB, then it may become handy if not necessary to execute the tests for this code on the actual server as opposed to running it in a mocked unit test environment. Benefits?
  • Your tests run in the same environment (or similar) as your production code.
  • In certain cases, it actually makes testing possible, because even when using mocking certain JEE6 behaviours are hard to simulate in a unit test.
JBoss 5 was so slow... But with JBoss 7 it doesn't take so much time to start up the server. So tests can run as a part of your Maven build or inside your IDE.

Configuration

Here I will show you how to set it all up. I did it myself, it works, and you can checkout the source yourself from GitHub.

What are we testing?

First, let's take a look at the class under test.
import javax.ejb.Local;
import javax.ejb.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import me.m1key.audiolicious.domain.entities.Song;
import me.m1key.audiolicious.services.SongRepository;

@Singleton
@Local(SongRepository.class)
public class JpaSongRepository implements SongRepository {

   @PersistenceContext
   private EntityManager entityManager;

   @Override
   public void save(Song song) {
       entityManager.persist(song);
   }
}
As you can see, it does not do a lot - but it is an EJB and it uses @PersistenceContext. I did not expose any way to set the entityManager because I don't like having to change production code just so that tests can run. So, that would be rather hard to mock. But we don't have to mock, we are going to run the test on the server.

The test

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.IOException;
import java.util.Date;

import javax.inject.Inject;

// Certain imports omitted for brevity.

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.resolver.api.DependencyResolvers;
import org.jboss.shrinkwrap.resolver.api.maven.MavenDependencyResolver;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class JpaSongRepositoryIT {

   // Stuff omitted for brevity.
   
   private Date albumDateAdded = new Date();
   private Date albumDateModified = new Date();
   private Date albumDateSkipped = new Date();

   @Inject
   private SongRepository jpaSongRepository;
   @Inject
   private RepositoriesTestHelperBean testHelperBean;

   @Deployment
   public static WebArchive createTestArchive()
           throws IllegalArgumentException, IOException {
       return ShrinkWrap
               .create(WebArchive.class,
                       JpaSongRepositoryIT.class.getSimpleName() + ".war")
               .addAsWebInfResource(EmptyAsset.INSTANCE,
                       ArchivePaths.create("beans.xml"))
               .addAsResource("log4j.xml", "log4j.xml")
               .addAsResource("META-INF/persistence.xml",
                       "META-INF/persistence.xml")
               .addClasses(Album.class, Artist.class, JpaSongRepository.class,
                       NullAlbum.class, NullArtist.class,
                       NullEntitiesFactory.class, Rating.class,
                       RatingTo.class, Song.class, SongRepository.class,
                       SongTo.class, RepositoriesTestHelperBean.class,
                       TrackTo.class)
               .addAsLibraries(
                       DependencyResolvers
                               .use(MavenDependencyResolver.class)
                               .artifacts("org.slf4j:slf4j-api:1.6.1",
                                       "org.slf4j:slf4j-log4j12:1.6.1",
                                       "commons-lang:commons-lang:2.6")
                               .resolveAsFiles());
   }

   @Test
   public void shouldCreateAndRetrieveSong() {
       assertEquals("There should be no songs before any are created.",
               Long.valueOf(0), testHelperBean.totalSongs());

       Artist artist = testHelperBean.createArtist(ARTIST_NAME);
       Album album = testHelperBean.createAlbum(ALBUM_NAME, artist,
               new Rating(80));
       Song song = new Song(SONG_1_NAME, ARTIST_NAME, album, 1988,
               "Zakk Wylde/Bob Daisley/Ozzy Osbourne", "Rock", albumDateAdded,
               albumDateModified, new Rating(80), 9, albumDateSkipped, 0,
               false, 0, 0, false);
       jpaSongRepository.save(song);

       assertNotNull("Saved song should not be null.",
               testHelperBean.getSongByUuid(song.getUuid()));
   }
  
   // Other tests omitted for brevity.
}
Let's take a closer look. First, the test is annotated with @RunWith(Arquillian.class). That is because we need Arquillian to orchestrate the entire test. Arquillian will handle our @Deployment. This is where, using ShrinkWrap, we create the WAR file with our test and what we want to test with all its dependencies, also the external ones, from our pom.xml. This WAR file will be deployed to JBoss 7 when you run the test with Maven or from within your IDE. I must admit that getting the dependencies right (i.e. giving it exactly what it needs and not too much) can be a bit tiresome and frustrating.

Dependencies

Below are the relevant dependencies.
       <dependency>
           <groupId>javax.inject</groupId>
           <artifactId>javax.inject</artifactId>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>org.jboss.spec</groupId>
           <artifactId>jboss-javaee-web-6.0</artifactId>
           <type>pom</type>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>org.jboss.as</groupId>
           <artifactId>jboss-as-arquillian-container-managed</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.jboss.shrinkwrap</groupId>
           <artifactId>shrinkwrap-api</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.jboss.shrinkwrap.resolver</groupId>
           <artifactId>shrinkwrap-resolver-impl-maven</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>junit</groupId>
           <artifactId>junit</artifactId>
           <scope>test</scope>
       </dependency>
       <dependency>
           <groupId>org.jboss.arquillian.junit</groupId>
           <artifactId>arquillian-junit-container</artifactId>
           <scope>test</scope>
       </dependency>

arquillian.xml

You also need to provide an arquillian.xml file in your src/test/resources. Below is my entire file.
<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns="http://jboss.org/schema/arquillian"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://jboss.org/schema/arquillian
       http://jboss.org/schema/arquillian/arquillian_1_0.xsd">

   <!-- Uncomment to have test archives exported to the file system for inspection -->
   <engine>
       <property name="deploymentExportPath">target/</property>
   </engine>

   <container qualifier="jboss" default="true">
       <protocol type="jmx-as7">
           <property name="executionType">REMOTE</property>
       </protocol>
   </container>

</arquillian>
When the test is running, it's running on a remote instance of JBoss. That means you have to download JBoss 7 yourself. You can hardcode its location in this file, but it's not a good idea if you're sharing the project with other people. Instead, you can set your JBOSS_HOME system variable to something like e:\Servers\jboss-as-web-7.0.0.Final.

Download sample source code

As always, working code is available for you to download. Get it now from GitHub. There is a text file called SETUP that explains how to get it going (the database and so on). If you have any questions, feel free to post them here and I will do my best to answer.

2011-10-17

jQuery UI Autocomplete + Spring MVC

jQuery UI offers a nice autocomplete component. In this short tutorial I will show you how to use it with Spring 3 MVC back end.

The view

Let's look at the view first. I'm using Velocity here and below you can only see a tiny bit of the file.
       <form method="post" action="">
           <table>
               <tr>
                   <td>Your name:</td>
                   <td>#springFormInput("reservation.userName" "")
                   </td>
                   <td>#springShowErrors("" "")
                   </td>
               </tr>
This will create an input field with an ID userName.

HTML metadata

<link type="text/css"

href="../css/humanity/jquery-ui-1.8.16.custom.css"
   rel="stylesheet" />
<link type="text/css" href="../css/basic.css" rel="stylesheet" />
<script type="text/javascript"

src="../js/jquery-1.6.2.min.js"></script>
<script type="text/javascript"

src="../js/jquery-ui-1.8.16.custom.min.js"></script>
<script>
   $(function() {
       $( "input:submit" ).button();
   });
</script>
<style>
   .ui-autocomplete-loading { background:

white url('../images/ui-anim_basic_16x16.gif') right center no-repeat; }
</style>
<script type="text/javascript" src="../js/autocomplete.js"></script>
Only the last line is non-standard. That's my custom JS file.

autocomplete.js

That's my custom JS file based heavily on the example jQuery UI have on their site.
$(function() {
   $("#userName").autocomplete(
       {
           source : function(request, response) {
               $.ajax({
                   url : "names",
                   dataType : "json",
                   data : {
                       term : request.term
                   },
                   success : function(data) {
                       response($.map(data.names, function(item) {
                           return {
                               label : item.label,
                               value : item.label
                           }
                       }));
                   }
               });
           },
           minLength : 1
       });
});
Let's explain it step by step. On windows load, the nameless function will run. Using jQuery selector I'm selecting the HTML element with ID userName. Then I'm calling the autocomplete() function on it that will turn it into an autocomplete input. It accepts many parameters; here I only use source and minLength because they are sufficient.
minLength specifies the content length threshold for back end query trigger. 1 means you only have to type 1 character to get suggestions.
source declares a function to handle response. That gives us the flexibility we need to handle our JSON response which looks like that:
{"names":[{"label":"Michael","value":"1"},{"label":"Mike","value":"2"},{"label":"Mikey","value":"3"}]}
This bit:
$.map(data.names, function(item)
… selects the names field from the response. Then, we have to select the label and value. Label is what will be displayed in the suggestion box. Value is what will be ultimately selected. In my case I chose to ignore the returned value (which is a number, as you can see) and just use the label field.

Back end - the controller

@Controller
@RequestMapping("/names")
public class NamesController {

   private static final Logger log = LoggerFactory
           .getLogger(NamesController.class);

   @RequestMapping(method = RequestMethod.GET)
   public String getNames(Model model,
           @RequestParam(required = false) String term) {
       log.debug("getNames(\"{}\")", term);
       model.addAttribute("names", Arrays.asList(names));
       return "jsonNames";
   }

   private LabelValueTo[] names = new LabelValueTo[] {
           new LabelValueTo("Michael", "1"), new LabelValueTo("Mike", "2"),
           new LabelValueTo("Mikey", "3") };

}
I have already written a bit about Spring and JSON so I will not comment that in great detail. Notice that I'm returning a view called jsonNames. What happens with it next?

Back end - view resolvers

I have these 2 view resolvers declared (there is more configuration to it - to get it download the source).
    <bean class="org.springframework.web.servlet.view.XmlViewResolver">
       <property name="order" value="0" />
       <property name="location" value="/WEB-INF/views.xml" />
   </bean>

   <bean id="viewResolver"
       class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
       <property name="order" value="1" />
       <property name="cache" value="true" />
       <property name="prefix" value="/WEB-INF/vm/" />
       <property name="suffix" value=".vm" />
       <property name="exposeSpringMacroHelpers" value="true" />
   </bean>
And the referenced view.xml file:
<bean name="jsonNames"
       class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />

Summary

And that's it. I think it's pretty straightforward. Feel free to post comments and questions. Download the source from GitHub.