23 Jul 2011

Selecting Separate Mobile and Desktop Interfaces with Grails

I'd like to create a Grails application that has both mobile and desktop browser interfaces. For the mobile interface, I've chosen jQuery Mobile. I found two blog postings that were very helpful. One by Graham Daley and the other by Bryan Hughes. Eventually, I'd like to have pages that are also designed for tablets and I plan on using jQuery Mobile for that also along with the jQuery-based Datatables. Omar Marji has a very good post on using jQuery Mobile with Grails.


Here are the main questions I wanted to answer:

1) How to detect that a user is on a mobile vs. desktop device. For this, I chose WURFL. I considered Modernizr for which there is a Grails Plugin and a good post by Ike Lin on it's use in a server side Grails implementation. Rob Fletcher is also a fan of Modernizr and made a presentation on this subject at the GR8 conference this year in Copenhagen.

2) How to intercept user requests and direct them to the Mobile or Desktop page layouts appropriately. I chose the approach described by Graham Daley to extend the sitemesh getDecorator interface that is used by Grails.

3) How to direct mobile users to a different mobile version of the views - list_m.gsp instead of list.gsp.

I'll walk through the setup for each of these for a Grails project.

Setting up WURFL with Grails

An important note before we begin. I initially attempted to use the latest 1.3.x release of WURFL but eventually gave up. I have a note below on one change that was required for 1.3.x but I still was not able to get it to work. Please comment below if you know about the changes required for WURFL 1.3.x
- Download the latest WURFL.XML zip file from SourceForge and place it in your projects src/java directory
- Also download the web_browsers_patch.xml file to the same directory the patch file to recognize desktop browsers.
- Download the WURFL Java API version 1.2.2 and place the file in your projects lib directory
- Edit the grails-app/conf/spring/resources.groovy and insert the code below in the beans block:

One note, there was a change in the 1.3.x release of WURFL that required a change in the resources.groovy since Graham Daley and Bryan Hughes blog postings. Change the reference to net.sourceforge.wurfl.core.resource.SpringXMLResource to net.sourceforge.wurfl.spring.SpringXMLResource.

beans = {
xmlns util: "http://www.springframework.org/schema/util"

// WURFL
// Groovy resource implementation of classpath:spring-wurfl.xml
wurflResource(org.springframework.core.io.ClassPathResource, "/wurfl-latest.zip") {
}
wurflSpringResource(net.sourceforge.wurfl.core.resource.SpringXMLResource, ref("wurflResource")) {
}
wurflModel(net.sourceforge.wurfl.core.resource.DefaultWURFLModel, ref("wurflSpringResource")) {
}
wurflMatcherManager(net.sourceforge.wurfl.core.handlers.matchers.MatcherManager, ref("wurflModel")) {
}
wurflDeviceProvider(net.sourceforge.wurfl.core.DefaultDeviceProvider, ref("wurflModel")) {
}
wurflService(net.sourceforge.wurfl.core.DefaultWURFLService, ref("wurflMatcherManager"), ref("wurflDeviceProvider")) {
}
wurflManager(net.sourceforge.wurfl.core.DefaultWURFLManager, ref("wurflService")) {
}
wurflUtils(net.sourceforge.wurfl.core.WURFLUtils, ref("wurflModel")) {
}
wurflHolder(net.sourceforge.wurfl.core.DefaultWURFLHolder, ref("wurflManager"), ref("wurflUtils")) {
}
}

WURFL is now installed.

Setting the Grails Layout for Mobile and Desktop users

We'll now install the code that will intercept the user connections, call WURFL to determine the browser type and select the layout to be used.
- mkdir -p src/groovy/com/grahamdaley/grails/mobile
- In this new directory, create a file called MobileDecoratorMapper.groovy and paste the following code
into the file:

/**
* MobileDecoratorMapper
* Copyright 2011 Graham Daley
* Released under the terms of the
* GNU General Public License version 2 (GPLv2)
*/
package com.grahamdaley.grails.mobile

import javax.servlet.http.HttpServletRequest
import com.opensymphony.module.sitemesh.*
import org.codehaus.groovy.grails.web.sitemesh.GrailsLayoutDecoratorMapper
import org.apache.commons.lang.StringUtils

/**
* MobileDecoratorMapper
*
* @author gdaley
*/
class MobileDecoratorMapper extends GrailsLayoutDecoratorMapper {
public Decorator getDecorator(HttpServletRequest request, Page page) {
MobileService mobileService = new MobileService()
String viewSuffix = mobileService.isMobileUser(request) ? ".mobile" : ""
Decorator decorator = super.getDecorator(request, page)
if (null == decorator) {
decorator = getNamedDecorator(request, "main" + viewSuffix)
}
else {
String decoratorName = decorator.getName()
decorator = getNamedDecorator(request, decoratorName + viewSuffix)
}

return decorator
}
}

- Also in the directory src/groovy/com/grahamdaley/grails/mobile
- Create a file called MobileService.groovy and paste the following code
into the file:

/**
* MobileDecoratorMapper
* Copyright 2011 Graham Daley
* Released under the terms of the
* GNU General Public License version 2 (GPLv2)
*/
package com.grahamdaley.grails.mobile

import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpSession
import net.sourceforge.wurfl.core.Device
import org.apache.commons.lang.StringUtils
import org.codehaus.groovy.grails.web.context.ServletContextHolder
import org.springframework.web.context.support.WebApplicationContextUtils
import org.springframework.context.ApplicationContext
/**
* MobileDecoratorMapper
*
* @author gdaley
*/
class MobileService {
def wurflManager

def isMobileUser(HttpServletRequest request) {
boolean isMobile = false
HttpSession session = request.session

// Is the preference already set?
if (request.getParameter("mobile") != null) {
// Did the user explicitly request a mobile / non-mobile format
// in the request parameters?
if (request.getParameter("mobile").equals("y")) {
session.isMobile = "y"
isMobile = true
}
else {
session.isMobile = "n"
}
}
else if (session.isMobile != null) {
// Did we decide this one in an earlier request?
if (session.isMobile.equals("y")) {
isMobile = true
}
}
else {
// If not, detect the browser type
if (isMobileBrowser(request)) {
session.isMobile = "y"
isMobile = true
}
else {
session.isMobile = "n"
}
}

return isMobile
}

def isMobileBrowser(HttpServletRequest request) {
if (wurflManager == null) {
def ctx = WebApplicationContextUtils.getWebApplicationContext(ServletContextHolder.servletContext)
wurflManager = ctx.getBean("wurflManager")
}

def Device device = wurflManager.getDeviceForRequest(request)
def String capability = device.getCapability("mobile_browser")
return StringUtils.isNotBlank(capability)
}
}

Configure Sitemesh to Invoke MobileDecoratorMapper

Edit the settings in web-app/WEB-INF/sitemesh.xml and change the line bracketed by decorator-mappers to point to the new MobileDecoratorMapper

<decorator-mappers>
<mapper class="com.grahamdaley.grails.mobile.MobileDecoratorMapper" />
</decorator-mappers>

Generate Separate Mobile and Desktop Templates and Views

The steps above have resulting in the Layout being set to a mobile layout for mobile users. However, the individual views are still using a desktop browser view and I have custom jQuery Mobile based views for the mobile users. I want mobile users to be re-directed to a separate view_m.gsp.

I'm going to use Spring Interceptors to intercept the browser calls and modify the view name to add the"_m" for mobile users. There's a good post and follow-on discussion about why this approach is better than Grails Filters in a post by Adam Monsen. The spring interceptors feature is described by VaanNila and for it's use in Grails I found this older post by Burt Beckwith helpful.

At the end of grails-app/conf/spring/resources.groovy insert the following in the beans block

beans = {
mobileInterceptor(com.flnkr.interceptor.MobileInterceptor)
}

I also created the file src/java/com/flnkr/interceptor/MobileInterceptor.java with the following:

package com.flnkr.interceptor;

import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

/**
*/
public class MobileInterceptor extends HandlerInterceptorAdapter {

static Logger logger = Logger.getLogger(MobileInterceptor.class);

static {
BasicConfigurator.configure();
}

@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
logger.info("After handling the request");
if (modelAndView != null) {
HttpSession session = request.getSession();
String isMobile = (String) session.getAttribute("isMobile");
if (isMobile == "y") {
modelAndView.setViewName(modelAndView.getViewName() + "_m");
}
}
super.postHandle(request, response, handler, modelAndView);
}
}

That's it. Now you'll need to create _m.gsp versions of your views for mobile users. With one project I had to do a grails clean for the changes to take effect.


Tags:
0 comments