Tuesday, September 12, 2006

Rolling with Spring and Hibernate, part 3: controller and view

So, now that we have a domain model with CRUD ability, we move forward to the controller and view parts. In the spirit of "convention over configuration", Spring 2.0 introduces a brand new URL mapper (the component responsible for determining what url (patterns) are handled by which controller): the ControllerClassNameHandlerMapping. Basically, it maps /foo/* requests too FooController, if FooController is a MultiActionController. We're going to write exactly such a RecipeController for the operations list, create, edit, store and delete.

For the views, I've chosen Freemarker, a fast and expressive template engine that has all the benefits of Velocity, the ability to use JSP taglibs and much more.

Additionally, I've chosen to add the OpenSessionInViewInterceptor, since all Rails relationships are lazy by default. It will allow Hibernate to load relations on the fly when needed, by keeping a session open during the request.

Rails controllers work exactly like the ControllerClassNameHandlerMapping, which is very handy since whenever you need an action /foo/doFunkyStuff, you just add that method to FooController and it will be exectued on those requests. Rails view are (always) .rhtml, which is Ruby scriptlets embedded in HTML, so both the handler mapping and the view configuration are things that work out of the box in Rails. Here's what our view and handler mapping configuration will look like (it goes into applicationContext-mvc.xml and cookbook-servet.xml, respectively):

<bean id="freeMarkerConfigurer" class="org.springframework...FreeMarkerConfigurer">
<property name="configLocation" value="/WEB-INF/classes/freemarker.properties" />
<property name="templateLoaderPath" value="/WEB-INF/freemarker" />
</bean>

<bean id="viewResolver" class="org.springframework...FreeMarkerViewResolver">
<property name="prefix" value="" />
<property name="suffix" value=".ftl" />
<property name="exposeSpringMacroHelpers" value="true" />
<property name="requestContextAttribute" value="rc" />
<property name="contentType" value="text/html; charset=UTF-8" />
</bean>

<bean id="openSessionInViewInterceptor" class="org.springframework...OpenSessionInViewInterceptor" />

There is a freemarker.properties which you can use to specify auto includes/imports, date formatting and some other things, but the defaults should do fine here.

Finally, the handler mapping:

<bean class="org.springframework...ControllerClassNameHandlerMapping">
<property name="interceptors" ref="openSessionInViewInterceptor">
</property>

<bean id="recipieController" class="se.petrix.cookbook.controller.RecipeController">

which is, as you can see, basically nothing. We simply rely on the class name, and add the OSIV interceptor to all handlers. The "interceptors" property is actually a list, but when you have a list with only one element, you can put it in a value or ref attribute.

We'll put our Freemarker templates in /WEB-INF/freemarker/recipe, and since we don't have any view scaffolding, we need to write them ourselves. One view for listing, and one view that we use for both edit and create. Freemarker does not handle null values, so we're going to make use of the "?if_exists" built-in, which expands to a GeneralPurposeNothing, a wrapper that usually has the meaning that you think it has, i.e. ${myList?if_exists} becomes an empty list, ${myString?if_exists} becomes an empty string and so on. Here's how we list all recipies:

<#list recipies?if_exists as r>
<tr>
<td>#{r.id}</td>
<td>${r.title?if_exists}</td>
<td>${r.description?if_exists}</td>
<td>${r.instructions?if_exists}</td>
<td>${(r.date.time)?if_exists?date}</td>
<td>${(r.category.name)?if_exists}</td>
<td><a href="${rc.contextPath}/recipie/edit.htm?id=#{r.id}">Edit</a></td>
<td><a href="${rc.contextPath}/recipie/delete.htm?id=#{r.id}">Delete</a></td>
</tr>
</#list>

Here you can see the date-formatter that works on a java.util.Date (?date). Another important thing is that Freemarker formats numbers by Locale when you use ${someNumber} ("1,000" in US locale vs. "1 000" in Swedish), but prints it as a mathematical number when you use #{someNumber} ("1000"). The "rc" varable is Spring's RequestContext, a wrapper around the HttpServletRequest that you can use for accessing the context path for example.

Note: as of Freemarker 2.3.7, the "?if_exists" builtin has been superceded by the shorthand notation "!".

Now there's only one thing left: web.xml. Again, Rails does not need one, but it's not a big deal, since you rarely make any big changes to it after the first few weeks of development, and you most likely already have a skeleton to start from (if you don't, you can use mine). It says "alright, we have a Spring servlet, which we want to hand *.htm requests to, and the Spring context is specified in the /WEB-INF/classes/applicationContext-*.xml files". As with all XML files in this example application, it has a corresponding XSD which smart editors can utilize to provide syntax checking an element and attribute completion.

Package the web app inside the source directory:

mvn war:inplace

The first time you run any Maven command it will download a large number of jars, many of which are plugins. It's all cached locally, so don't worry. On deploy, Hibernate will create your database schema for you.

AspectJ load-time weaving requires a parameter to be passed to the Java VM. This can be accomplished by setting the shell variable MAVEN_OPTS before starting Jetty:

export MAVEN_OPTS=-javaagent:src/main/webapp/WEB-INF/lib/aspectjweaver-1.5.2a.jar

Adapt to your shell of choice. Now we're ready to start the Jetty server:

mvn jetty:run

Watch the magic at http://localhost:8080/cookbook/recipe/list.htm :-)

The scan interval is set to 3 seconds, and since we're packaging the webapp inside the source directory, any changes to non-Java files are instantly available, and changes to Java files will trigger a context reload on recompilation (which your IDE should do for you). Try the old space-backspace-save trick to trigger a recompilation, and you should see a context reload.

In part 4, we'll add the Category model and relationship between Recipe and Category.

No comments: