Tuesday, January 25, 2011

Marshal Json data using Jackson in Spring MVC with Jaxb Annotations

The title is quite a mouthful but let me explain...For the DevNexus.com website I want to provide the website's data not only via XML but also via JSON in order to provide maximum flexibility for other consuming services. E.g. Pratik Patel pinged me the other day that he intends building an IPad client and Charlie Collins is planning to update his DevNexus Android client, which he originally created for the 2010 conference (The pressure is on gentleman ;-).

The DevNexus website is based on Spring MVC 3.0 and provides RESTful Urls that render Html views of the data (Targeting traditional browser clients as well as mobile client using Spring Mobile and jQuery Mobile).

Thus, I configured Restful endpoints as described in my last blog post. The easiest way to marshal JSON in Spring MVC is via the Jackson Java JSON-processor. Spring has a few documentation pointers regarding that. Theoretically, the setup is fairly simple and in its simplest forms means to just put the Jackson jar file onto your class path and adding mvc:annotation-driven to your Spring Xml Context file (See Chapter 15.12.1 of the Spring documentation). No sweat.

To get started more specifically with Jackson, here are some blog entries that I came accross while diving deeper into it:
While the tutorials may make you think that serializing JSON is happening defacto automatically, it turned out, that I had to fine-tune my domain model for Jackson. My domain model objects have bi-directional relationships and and I was getting a ton of errors of type:

java.lang.StackOverflowError - org.codehaus.jackson.map.JsonMappingException: Infinite recursion (StackOverflowError)

You can configure Jackson's marshalling behavior by annotating your domain model objects. However, those annotations are Jackson specific and I already was annotating my obejects with Jaxb annotations for the Xml marshalling of data.

Well - cool beans - , it turns out, Jackson can indeed use Jaxb annotations to renders Json data (added with version 1.1 of Jackson).

But how do I configure Jackson? How do I make it work in my Spring environment? I came across 2 blogs, that combined, provided the necessary solution.

The first example I found here, which provides a really nice example.

Furthermore, I not only wanted to configure Jackson to use Jaxb annotations, but I also needed to make it work for my Spring environment. For that Kyrill Alyoshin has some good information on his blog (
He also details how to make Jackson work better with Hibernate, which sounds interesting and I may return to his blog entry more specifically for that.).

Basically the best way to integrate Jackson and Spring is to create a custom ObjectMapper class that enables Jaxb support and which can then be injected into Spring's MappingJacksonJsonView.

Here are the things that were needed to get things going for my project -

First, add the following Maven Dependencies (jackson-xc provides the additional Jaxb support):
<!-- Jackson JSON Mapper -->
 <dependency>
     <groupId>org.codehaus.jackson</groupId>
     <artifactId>jackson-core-lgpl</artifactId>
     <version>1.6.4</version>
 </dependency>
 <dependency>
     <groupId>org.codehaus.jackson</groupId>
     <artifactId>jackson-mapper-lgpl</artifactId>
     <version>1.6.4</version>
 </dependency>
 <dependency>
     <groupId>org.codehaus.jackson</groupId>
     <artifactId>jackson-xc</artifactId>
     <version>1.6.4</version>
 </dependency>

The custom ObjectMapper:
import org.codehaus.jackson.map.AnnotationIntrospector;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.xc.JaxbAnnotationIntrospector;

public class JaxbJacksonObjectMapper extends ObjectMapper {

 public JaxbJacksonObjectMapper() {
  final AnnotationIntrospector introspector
      = new JaxbAnnotationIntrospector();
  super.getDeserializationConfig()
       .setAnnotationIntrospector(introspector);
  super.getSerializationConfig()
       .setAnnotationIntrospector(introspector);
    }

} 

The SpringContext configuration that wires everything together:
    <bean class="org.springframework.web.servlet.view
        .ContentNegotiatingViewResolver">
        <property name="order" value="1" />
        <property name="ignoreAcceptHeader" value="true" />
        <property name="mediaTypes">
            <map>
                <entry key="xml"  value="application/xml"/>
                <entry key="json" value="application/json"/>
            </map>
        </property>
        <property name="defaultViews">
            <list>
                <bean class="org.springframework.web.servlet.view
          .xml.MarshallingView">
                    <property name="marshaller" 
                      ref="jaxbMarshaller"/>
                </bean>
                <bean
                    class="org.springframework.web
.servlet.view.json.MappingJacksonJsonView">
                    <property name="objectMapper"
 ref="jaxbJacksonObjectMapper"/>
                </bean> 
            </list>
        </property>
    </bean>

Friday, January 21, 2011

REST with Spring - ContentNegotiatingViewResolver vs. HttpMessageConverter+ResponseBody Annotation

Disclaimer: This blog posting reflects experiences while learning to implement RESTful services. Thus, certain elements of my blog posting may turn our to be not correct. Therefore, if you encounter errors, please let me know and I will post corrections as soon as possible. Please proceed with caution...

As I posted in my previous blog "Making DevNexus.com more Restful", I am in the process of making more of the data from DevNexus.com consumable by other services, by exposing JSon and XML based endpoints. The website/application is implemented using Spring MVC 3.0 in the view layer using Spring's REST support.

Here are a few good reads that I came across that provide some helpful information:
  1. http://blog.springsource.com/2009/03/08/rest-in-spring-3-mvc/
  2. http://blog.springsource.com/2009/03/16/adding-an-atom-view-to-an-application-using-springs-rest-support/
  3. http://rwehner.wordpress.com/2010/06/09/2-ways-to-create-json-response-for-ajax-request-in-spring3/
  4. http://www.ibm.com/developerworks/web/library/wa-restful/
  5. http://forum.springframework.org/showthread.php?p=337206
  6. http://static.springsource.org/spring/docs/3.0.x/reference/mvc.html
As it turns out, there are 2 ways of implementing REST endpoints:
Using a ContentNegotiatingViewResolver

When you use a ContentNegotiatingViewResolver your web controllers return ModelAndViews or view names and the ContentNegotiatingViewResolver will, based on various criteria, choose the right data representation strategy.

The highest priority hereby has the file extension which is used if available in the request. Next, the ViewResolver will look for a (definable) request parameter that identifies the view. If that does not help, the ViewResolver uses the Java Activation Framework to determine the Content-Type. If all fails, use the the HTTP Accept header. Of course the steps can be individually disabled.

A key takeaway though is, that your controllers will return a single ModelAndView/Viewname that will resolve into a specific view such as:
  • org.springframework.web.servlet.view.json.MappingJacksonJsonView
  • org.springframework.web.servlet.view.xml.MarshallingView 
  • org.springframework.web.servlet.view.documentClass.AbstractPdfView
  • etc...
However, this may feel a little unnatural for certain data representations such as XML (using Jaxb annotations) or Json (using Jackson), where a dedicated view may not be necessary. Luckily, you can configure the ContentNegotiatingViewResolver to use default views which kind of solves the issue.

Using HttpMessageConverters

This is where HttpMessageConverters potentially could help. Whenever you use the @ResponseBody annotation you will be using a HttpMessageConverter (See also the Spring reference documentation, chapter "15.3.2.5 Mapping the request body with the @RequestBody annotation" and "18.3.2 HTTP Message Conversion").

What this means is, that instead of returning a ModelAndView or view name, you will actually return data, e.g. a Collection of Objects.

If you use the Spring Context  <mvc:annotation-driven/> then support for XML and JSON marshalling is activated by default, provided the respective class libraries (Jaxb and/or Jackson) are present in your class-path (See the Spring documentation for details at chapter "15.12.1 mvc:annotation-driven")

Here is the interesting problem, if you use <mvc:annotation-driven/> you can set additional conversion services but you cannot set additional HttpMessageConverters. Consequently, if you want to do that then you have to use explicit bean declarations and remove the <mvc:annotation-driven/> tag from your context. This was something not too well covered in article [6] http://www.ibm.com/developerworks/web/library/wa-restful/

Ultimately, I am using the following bean declaration instead of the <mvc:annotation-driven/>  tag:

     <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="webBindingInitializer">
            <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
                <property name="conversionService" ref="conversionService"/>
                <property name="validator" ref="validator"/>
            </bean>
        </property>
       <property name="messageConverters">
           <list>
               <ref bean="jsonConverter" />
               <ref bean="marshallingConverter" />
               <ref bean="atomConverter" />
           </list>
       </property>
    </bean>

My choice

This was a bit tricky. Both approaches are somewhat overlapping and I wish the documentation would give you better guidance on which approach to use under which cicumstances. Certainly the ContentNegotiatingViewResolver option seems to be the better documented solution. On the other side, you don't need to configure explicit views when using HttpMessageConverters and the ResponseBody annotation and therefore, that setup looks a bit more streamlined.

One issue I was running into was, that for my application I can return both pure data views (Json, XML) and also Html/Jsp responses. Somehow I was not able to configure my controller easily+cleanly, to respond to the same Url with multiple controller methods (one using @ResponseBody and the other returning a JSP view)

Secondly, I wanted to also support file extensions to use the correct view or converter.  As it turns out though, HttpMessageConverters don't support that - Although there was an example somewhere for using request parameters. But that approach would require me creating additional custom classes...

Anyway, I went ahead chose the ContentNegotiatingViewResolver approach to implement my Restful services.

Further issues

Once I made my decision, everything seemed to go smoothly. I got my services implemented quickly and they worked perfectly in Firefox. Here is an example of the intended Url structure:

http://www.devnexus.com/s/speakers
http://www.devnexus.com/s/speakers.html
http://www.devnexus.com/s/speakers.xml
http://www.devnexus.com/s/speakers.json

With that I thought to have covered all bases: Support file extensions, but also allow clients to connect to http://www.devnexus.com/s/speakers and retrieve all data representations using the respective Http Accept header.

I deployed the app into production, but then the next day at the monthly Atlanta Users Group meeting - people informed me that the DevNexus site were down. That was odd, as I had accessed the site just minutes prior to the meeting.

Well, as it turned out, Google Chrome, Safari and Internet Explorer transmit a wild mixture of Http Accept headers. Consequently, when users accessed http://www.devnexus.com/s/index then the server would try to return an Xml view because Chrome and Safari requested Xml data rather than Html data.

For more details on Accept headers, please see the following fascinating:

http://www.gethifi.com/blog/browser-rest-http-accept-headers

What also threw me off, was that quite a few sources, incl. the Spring documentation imply that using the Http Accept header might be a actually a viable way of determining the correct view to return to clients.

To illustrate the chaos -Here is an interesting posting from the webkit mailing list:

https://lists.webkit.org/pipermail/webkit-dev/2010-January/011188.html

Ultimately, I got my servlet context configured in a way I think works best for me, though. The extension-less Url will always return Html now and for other data representations the file extension is mandatory. I also configured the ContentNegotiatingViewResolver to ignore Http Accept header by setting:
<property name="ignoreAcceptHeader" value="true" />
Many thanks to this blog posting by Rick Herrick. Thus, now my servlet-context.xml file contains:

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="order" value="1" />
        <property name="ignoreAcceptHeader" value="true" />
        <property name="mediaTypes">
            <map>
                <entry key="xml"  value="application/xml"/>
                <entry key="json" value="application/json"/>
            </map>
        </property>
        <property name="defaultViews">
            <list>
                <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
                    <property name="marshaller" ref="jaxbMarshaller"/>
                </bean>
                <bean
                    class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
                    <property name="objectMapper" ref="jaxbJacksonObjectMapper"/>
                </bean>
            </list>
        </property>
    </bean>

    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
        <property name="order" value="2"/>
    </bean> 
 
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
        <property name="order"  value="3"/>
    </bean>

That was a rather long day to get things working as intended but a good learning experience nonetheless. There is certainly something intriguing about having clean Urls.

Wednesday, January 19, 2011

Review Starfish Reef Resort, Bocas del Toro, Panama


Our family of four stayed at Starfish Reef for three and a half weeks at the end of 2010. We have two small children, ages two and three months. There were several things about our stay that were very nice, such as:
  • Our hosts, Jack and Kelly made dinner for us twice
  • Jack and Kelly called water taxis for us whenever we asked
  • A few times Jack drove us in his water taxi to places we requested (for a fee), and soon after we first arrived he took several hours to show us around Almirante and help us with our shopping there
  • Jack picked us up in his mule (a small ATV) the last few days of our trip. For the first three weeks of our stay it was unavailable
  • The snorkeling on Starfish Reef was very nice, and we saw an octopus, a manta ray, and lots of small fish
  • Jack and Kelly installed a safety latch on the outside door of the room our two year old slept in
  • Jack and Kelly recommended a lovely hotel in Panama City for our stay there, as well as a local tour guide who was knowledgeable and helpful
However, we would not recommend Starfish Reef for several reasons, including:
- Shepherd Island, where Starfish Reef is located, has a terrible chitra (sand fly or no-see-ums) problem. However, Starfish Reef only offers mosquito nets for sleeping. Importantly, chitras are much smaller than mosquitoes, so the nets do not provide much if any coverage from chitras. Jack and Kelly do recommend using smoke coils to keep the chitras away (which we bought at our expense). These smoke coils do work well, however, it is a very heavy smell and not something we were pleased to use around our children, much less ourselves. We were very irritated that readily available finer chitra-repelling netting wasn't used for the beds. And no netting was provided for our son's crib initially, we had to ask for it.
- Internet is very important to us, since my wife has her own business, and I work in the IT field. We specifically asked about internet coverage when deciding where we would stay in Bocas del Toro. Jack and Kelly informed us that their internet is in all of the houses. However, we had hardly any internet coverage for the first week we were guests at Starfish Reef. Gunnar repeatedly spoke with Jack about it, and Jack finally had it fixed approximately one week into our vacation. However, even then, it was not throughout our house. It was only available in certain parts of the house, excluding the living room and most of the main bedroom. And even once it was fixed, it still had almost daily outages.
- Jack and Kelly charge $6 for each load of laundry. Since we originally were going to stay with them for five weeks (more on that below), and since we have two small children, we were doing personal laundry twice a week. One garnet was ruined in each of our first several loads of laundry. We ended up losing five garments - four pieces of clothes and a crib sheet that we had for over two years, and which was now far too small to fit the pack and play. We did not do the laundry, it was done for us. After a new shirt given to our daughter was ruined (it had a hole in it that three pencils would fit through), we approached Jack about it. He said that he was aware that the washing machine had some problems, and he showed us the filthy shirt he was wearing as further proof of this. This boggled our minds. If he knew that his washing machine was not working properly and was in fact ruining clothes, why did he not previously tell us? And how could he charge us for this service?! And Jack actually suggested that we place a patch on our daughter's shirt to cover up the hole! When we told Kelly about our ruined items, she told us that we should not have brought nice clothes with us on vacation! When we told her that t-shirts and baby onesies are not "nice clothes", she had no response. And when we showed her a t-shirt of Gunnar's that had a large stain on it after they washing it, she said that it looked like it was worn already. Yes, Kelly actually said this to paying guests.
- Jack and Kelly provided us with written information which stated "Taxi Service: Bocas Marine Tours $8pp each way to Almirante or Bocas to and from Shepherd Island (minimum 2 persons or $16) until 6 pm. Night taxi service with Fabian." They neglected to tell us that Bocas Marine Taxi (BMT) may refuse to go to Shepherd Island in inclement weather. We learned this the hard way. One afternoon we arrived at the taxi port at 5:45pm expecting to make the 6pm taxi. Instead, we were told that the last water taxi had left around 4pm due to the inclement weather. We ended up paying $60.00 for a private taxi to take us back to Starfish Reef. The same trip with the water taxi service would have been $16.00 for all of us. Jack and Kelly had no response when we told them of this incident. When we were leaving Starfish Reef, Jack repeatedly said that they could not be expected to tell us about all of the issues we may have encountered while were were their guests. However, common sense dicates that they would have told us if the taxi service did not transport during inclement weather.
- Shepherd Island is about a 20 minute boat ride from Bocas del Toro in good weather. As stated above, Starfish Reef has, or had, a deal with BMT to transport its guests. This worked fine in good, and relatively good, weather. However, when the weather was poor, BMT did not want to come out to Starfish Reef. On at least two occasions, BMT said that it was not sure that it would be able to come out to pick us up. It did come out, but not at the stated times and much earlier than expected. We kept the taxi service waiting these two times because it arrived 10-15 minutes early. Thereafter, beginning on December 15th, BMT refused to pick us up. Jack and Kelly told us that the service told them that it was because we kept them waiting (which Jack explicitly told us he knew was not fair), and when we personally asked BMT, we were told that it was because of the inclement weather and the dangers it posed. Notably, BMT makes several trips during the day between Bocas del Toro and Almirante. Both locations provide it with several passengers. Going to Starfish Reef in bad weather, for two paying customers, likely was not worth the time, gas and potential danger. Regardless, the sudden lack of taxi service was a severe problem for us since it was our means of transportation. When this happened, Jack offered to bring us to Almirante to pick up BMT there. However, this would have increased our trip times by more than double, since Almirante was a 15-20 minute ride in Jack's boat, and we would then have to wait for the water taxi. So, a 20 minute trip would become a 45-50 minute trip, each way.
- We repeatedly had to remind Jack that our house needed weekly cleaning, and had to specifically ask that our linens be washed weekly. These are service that Jack and Kelly previously told us were included.
- Several of the steps leading to and from the dock to the houses are washed out. Since it is a fairly steep walk, and since we were walking it almost daily with two small children, we were disappointed that the steps were in such deteriorated and potentially dangerous condition.
- The written information provided to us by Jack and Kelly on the day we arrived states that they charge $10.00 per couple to go to Rana Azul, a restaurant about twenty minutes by boat. We were instead charged $16.00 per couple. We only learned that we were to have been charged $10.00 after we re-read the paperwork several days into our stay. If there was a rate change, we should have been informed of this prior to taking the trips.
- Jack invited us to join him for Thanksgiving dinner at Rana Azul. Immediately before the dinner was an at sea funeral for a friend and neighbor of his who recently passed away. Jack was going directly from the funeral to Rana Azul. Jack asked us several times if we would like to join him for both events. We had nothing else planned for the day, and since it was Thanksgiving, we agreed. After the dinner, Jack then asked us for $16.00 as our fare to Rana Azul (which should have been $10.00). Not once previously did he mention that there would be a charge. This annoyed us since he had several opportunities to tell us that he would charge us, AND because it was clear that he was going to the funeral and dinner afterwards with or without us. It certainly made us feel that we were being nicked and dimed by Starfish Reef.
- Jack and Kelly offer several excursions, with rates ranging from $20.00 to $550.00. We asked that they take us on the chocolate tour at Green Acres Farm no fewer than five or six times before they scheduled a visit for us. Notably, this is their cheapest excursion.
- The website states that the property has "a working green rainforest and Cacao plantation". This led us to believe that the property contained a cacao plantation. It does not. It has cacao trees, but no working cacao (or chocolate) plantation.
- The two-way radio between our house and Jack and Kelly's house often did not work because, as Jack and Kelly said, their radio "needed new batteries." We would then communicate by yelling to each other from the porches of our houses.

We originally planned to stay at Starfish Reef for five weeks, and we prepaid for five weeks. We decided to leave Starfish Reef five days early, on December 20th, because we were very disappointed with our lodgings and the property, and because we were spending so much time there due to the very rainy weather. If the weather was better and we were out of the house and off the island more, many of the problems stated above would not have bothered us as much. We asked for a refund for those five days, and were told that we would not get one because, as Jack said, "singly or collectively", the issues we raised did not qualify for a refund. We were shocked by this response in light of all of the problems we had during our stay.

We ended up leaving five days earlier than that because, on December 15th, both BMT stopped coming to Starfish Reef (as per the above), and because there was no water in our house. Jack had installed a new valve on the water pump on December 14th. The water was leaking for several hours after Jack's installation. We were gone that afternoon, and when we returned in the early evening and heard running water, we tried reaching Jack and Kelly to tell them that we thought there was a problem. We tried them on the two-way radio beginning at approximately 6pm. No one answered. We tried off and on until 11pm, but there was no answer. We also yelled for them from our balcony, to no avail. The next morning we woke them up at 6:30am by yelling at them from our balcony. When we told them that we had been trying to reach them all evening and into the night, Jack said that he turns off the two-way radio at night (meaning that we have no means of communicating with our hosts at night). When we told him that we tried reaching him in the early evening, he had no response. He later repeatedly asked us if we used up all of the water. We told him that we did not, that we were gone that afternoon, and that we heard the pump running for several hours and kept trying to inform him of it. It was evident to us that the problem likely stemmed from the work Jack did on the pump earlier that day, but he sought to blame us instead.

Two comments in closing. What bothered us the most was that Jack and Kelly did not accept responsibility for obvious problems at Starfish Reef - their laundry machine not working properly, their internet not working and/or not working throughout the entire house, the water pump, etc. They never once said "We're sorry, how can we make this up to you?" We travel quite a bit and usually do not have any complaints. Here, we were faced with many problems at Starfish Reef that made our stay far less enjoyable, and when we raised these issues, Jack and Kelly's response was always to deflect it back onto us.

Secondly, we would not recommend this property for a family or for anyone staying for more than a week. However, if the internet is not important to you, you are not doing a laundry, chitras don't bother you much, and BMT is traveling again to Starfish Reef, etc., you may enjoy this property. There is a lot of beauty there and the houses are spacious and modern. If you are looking to get away for a few days to a week and aren't bothered by what we wrote above, this may be the place for you. But for us, it was clearly far less than we were led to believe we would find. We were over-promised, and were very disappointed to find a dilapidated property, with hosts who do not take responsibility for problems on their property.

Alysa Freeman and Gunnar Hillert, January 2011.

Tuesday, January 18, 2011

Making DevNexus.com more Restful

A few people had ask me, whether I can make the data of DevNexus.com consumable for other clients (e.g. Android or iPhone). Sure, no sweat - As I expose the URL in a somewhat RESTful fashion already, I thought exposing a variety of different data representations is not hard but it turned out to be a bit more tricky than I thought but I will blog about that in the next 1-2 days or so.

Under the covers I am using Spring MVC 3.0 that provides the Rest infrastructure, Jackson for the Json marshalling and Jaxb for marshalling the XML data. 

First of all, here is what I have accomplished: The page that returns a list of presentations and the page that returns a list of speakers, will both return the data in either XML or JSON as well. Bear in mind this is still a fairly rough cut and the data representation may still change quite a bit.

Anyway, these are the exposed endpoints so far:

http://www.devnexus.com/s/presentations
Returns any of the other following 3 data representations depending of the Accept Http header. (However, a defined file extension always wins over the Accept header.)
  • http://www.devnexus.com/s/presentations.html
  • http://www.devnexus.com/s/presentations.xml
  • http://www.devnexus.com/s/presentations.json
http://www.devnexus.com/s/speakers
Returns any of the other following 3 data representations depending of the Accept Http header. (However, a defined file extension always wins over the Accept header.)
  • http://www.devnexus.com/s/speakers.html
  • http://www.devnexus.com/s/speakers.xml
  • http://www.devnexus.com/s/speakers.json 
As Accept headers the following values are acceptable and handled right now:
  • application/xml
  • application/json
  • text/html
For testing the Firefox plugin "Rest Client" works really well:  https://addons.mozilla.org/en-US/firefox/addon/restclient/

As mentioned above, I will blog about the technical details and encountered issues in the next couple of days. Also, if you try to consume the data and run into issues and or have suggestions - ping me and add a comment!

Tuesday, January 4, 2011

Spring Mobile, jQuery Mobile and SiteMesh

I just realized that I haven't published a blog entry in a while. Time just flies by. Anyway, I am in the midst with several other volunteers to prepare for the Atlanta-based DevNexus developer conference which will take place March 21-22.

For this year's conference I reworked the conference website application using Spring (MVC) 3.0, JPA 2.0 etc. In addition to standard clients, I started looking into supporting mobile clients as well.

My requirement was to provide a decent looking mobile interface in a reasonable amount of time (heck, it is all done during my spare-time afterall). Plus, I wanted to support multiple mobile devices at once. Out of that reason I ruled out a native client implementation (at least for now).

Another option, would have been to look at other development frameworks such as PhoneGap and Appcelerator, something I may still have to do down the road.

As I have been using jQuery quite a bit, I started exploring it using either jQTouch or jQuery Mobile. I opted into looking at jQuery Mobile as it is the official jQuery sanctioned project.

As it turns out, you can easily create create some nice looking UIs using jQuery Mobile. As an example, just point, your mobile browser, e.g. Android to http://www.devnexus.com/. This is pretty impressive considering that I have been using an Alpha 2 version with just standard theming and functionality.

I still have to work on the detail pages, but the main landing page looks fairly nice out of the box using minimal amounts of code.

On the server-side, I am using Spring Mobile, which currently is available as a M2 release. What Spring Mobile allows you to do is to detect from within your Spring MVC controllers, whether the incoming request originates from a mobile device or not.

If you need to detect specific device capabilities, you can even use a WurflDeviceResolver, but for my use case I simply need to know whether a mobile device request is coming in or not. If so, forward to a mobile version of the respective controller's jsp page.

One, question I had to solve was how to make everything work with SiteMesh, which I use for templating purposes. As I use the same Urls for mobile and standard clients (No site-switching), I ended up needing to create a custom SiteMesh DecoratorMapper extending AbstractDecoratorMapper.



public class SpringMobileParameterDecoratorMapper extends AbstractDecoratorMapper {
    private String decoratorName = null;

    public void init(Config config, Properties properties, DecoratorMapper parent) throws InstantiationException {
        super.init(config, properties, parent);
        decoratorName = properties.getProperty("decorator.name", "mobile");
    }

    public Decorator getDecorator(final HttpServletRequest request, final Page page) {

        final Device device = DeviceResolverHandlerInterceptor.getCurrentDevice(request);

        if (device != null && device.isMobile()) {
         return getNamedDecorator(request, decoratorName);
        } else {
         return super.getDecorator(request, page);
        }

    }
} 

It may not be the bestapproach to have a hard dependency onto Spring Mobile in my SiteMesh DecoratorMapper...but it works nicely so far.