© Marten Deinum, Daniel Rubio, and Josh Long 2017

Marten Deinum, Daniel Rubio and Josh Long, Spring 5 Recipes, https://doi.org/10.1007/978-1-4842-2790-9_8

8. Spring Mobile

Marten Deinum, Daniel Rubio2 and Josh Long3

(1)Meppel, Drenthe, The Netherlands

(2)F. Bahia, Ensenada, Baja California, Mexico

(3)Apartment 205, Canyon Country, California, USA

Today more mobile devices exist than ever before. Most of these mobile devices can access the Internet and can access web sites. However, some mobile devices might have a browser that lacks certain HTML or JavaScript features that you use on your web site; you also might want to show a different web site to your mobile users or maybe give them a choice of viewing a mobile version. In those cases, you could write all the device detection routines yourself, but Spring Mobile provides ways to detect the device being used.

8-1. Detect Devices Without Spring Mobile

Problem

You want to detect the type of device that connects to your web site.

Solution

Create a Filter that detects the User-Agent value of the incoming request and sets a request attribute so that it can be retrieved in a controller.

How It Works

Here is the Filter implementation you need to do device detection based on User-Agent:

package com.apress.springrecipes.mobile.web.filter;

import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;


import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class DeviceResolverRequestFilter extends OncePerRequestFilter {

    public static final String CURRENT_DEVICE_ATTRIBUTE = "currentDevice";

    public static final String DEVICE_MOBILE = "MOBILE";
    public static final String DEVICE_TABLET = "TABLET";
    public static final String DEVICE_NORMAL = "NORMAL";


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String userAgent = request.getHeader("User-Agent");
        String device = DEVICE_NORMAL;


        if (StringUtils.hasText(userAgent)) {
            userAgent = userAgent.toLowerCase();
            if (userAgent.contains("android")) {
                device = userAgent.contains("mobile") ? DEVICE_NORMAL : DEVICE_TABLET;
            } else if (userAgent.contains("ipad") || userAgent.contains("playbook") || userAgent.contains("kindle")) {
                device = DEVICE_TABLET;
            } else if (userAgent.contains("mobil") || userAgent.contains("ipod") || userAgent.contains("nintendo DS")) {
                device = DEVICE_MOBILE;
            }
        }
        request.setAttribute(CURRENT_DEVICE_ATTRIBUTE, device);
        filterChain.doFilter(request, response);
    }
}

This implementation first retrieves the User-Agent header from the incoming request. When there is a value in there, the filter needs to check what is in the header. There are some if/else constructs in the header to do basic detection of the type of device. There is a special case for Android because that can be a tablet or mobile device. When the filter determines what the type of device is, the type is stored as a request attribute so that it is available to other components. Next, there is a controller and JSP page to display some information about what is going on. The controller simply directs to a home.jsp page, which is located in the WEB-INF/views directory. A configured InternalResourceViewResolver takes care of resolving the name to an actual JSP page (for more information, refer to the recipes in Chapter 4).

package com.apress.springrecipes.mobile.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


import javax.servlet.http.HttpServletRequest;

@Controller
public class HomeController {


    @RequestMapping("/home")
    public String index(HttpServletRequest request) {
        return "home";
    }


}

Here’s the home.jsp page:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!doctype html>
<html>
<body>


<h1>Welcome</h1>
<p>
    Your User-Agent header: <c:out value="${header['User-Agent']}" />
</p>
<p>
    Your type of device: <c:out value="${requestScope.currentDevice}" />
</p>


</body>
</html>

The JSP shows the User-Agent header (if any) and the type of device, which has been determined by your own DeviceResolverRequestFilter.

Finally, here is the configuration and bootstrapping logic:

package com.apress.springrecipes.mobile.web.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.InternalResourceViewResolver;


@Configuration
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration {


    @Bean
    public ViewResolver viewResolver() {


        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

The controller is picked up by the @ComponentScan annotation. For bootstrapping the application, there is the MobileApplicationInitializer, which bootstraps DispatcherServlet and optionally ContextLoaderListener.

package com.apress.springrecipes.mobile.web;

import com.apress.springrecipes.mobile.web.config.MobileConfiguration;
import com.apress.springrecipes.mobile.web.filter.DeviceResolverRequestFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;


import javax.servlet.Filter;

public class MobileApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }


    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { MobileConfiguration.class };
    }


    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {new DeviceResolverRequestFilter()};
    }


    @Override
    protected String[] getServletMappings() {
        return new String[] {"/"};
    }
}

There are two things to notice here. First, the previously mentioned configuration class is passed to DispatcherServlet by implementing the getServletConfigClasses method. Second, the implementation of the getServletFilters method takes care of registering the filter and maps it to DispatcherServlet. When the application is deployed, using http://localhost:8080/mobile/home will show you the User-Agent value and what type the filter thinks it is (see Figure 8-1).

A314861_4_En_8_Fig1_HTML.jpg
Figure 8-1. Viewing the application in Chrome

Using Chrome on an iMac produces the result shown in Figure 8-1. When using an iPhone, it looks something like Figure 8-2.

A314861_4_En_8_Fig2_HTML.jpg
Figure 8-2. Viewing the application using an iPhone 4
Note

For testing different browsers, you can either use a tablet or mobile device on your internal network or use a browser plug-in such as User-Agent Switcher for Chrome or Firefox.

Although the filter does its job, it is far from complete. For instance, some mobile devices don’t match the rules (the Kindle Fire has a different header than a regular Kindle device). It is also quite hard to maintain the list of rules and devices or to test with many devices. Using a library like Spring Mobile is much easier than doing this on your own.

8-2. Detect Devices with Spring Mobile

Problem

You want to detect the type of device that connects to your web site and want to use Spring Mobile to help you with this.

Solution

Use the Spring Mobile DeviceResolver and helper classes to determine the type of device by configuring either DeviceResolverRequestFilter or DeviceResolverHandlerInterceptor.

How It Works

Both DeviceResolverRequestFilter and DeviceResolverHandlerInterceptor delegate the detection of the type of device to a DeviceResolver class. Spring Mobile provides an implementation of that interface named LiteDeviceResolver. The DeviceResolver class returns a Device object, which indicates the type. This Device object is stored as a request attribute so that it can be used further down the chain. Spring Mobile comes with a single default implementation of the Device interface, LiteDevice.

Use DeviceResolverRequestFilter

Using DeviceResolverRequestFilter is a matter of adding it to the web application and mapping it to the servlet or requests that you want it to handle. For your application, that means adding it to the getServletFilters method. The advantage of using this filter is that it is possible to use it even outside a Spring-based application. It could be used in, for instance, a JSF-based application.

package com.apress.springrecipes.mobile.web;
...
import org.springframework.mobile.device.DeviceResolverRequestFilter;


public class MobileApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
...
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {new DeviceResolverRequestFilter()};
    }
}

This configuration registers DeviceResolverRequestFilter and will automatically attach it to requests handled by DispatcherServlet. To test this, issue a request to http://localhost:8080/mobile/home, which should display something like the following:

A314861_4_En_8_Fig3_HTML.jpg

The output for the device is the text as created for the toString method on the LiteDevice class provided by Spring Mobile.

Use DeviceResolverHandlerInterceptor

When using Spring Mobile in a Spring MVC–based application, it is easier to work with DeviceResolverHandlerInterceptor . This needs to be configured in your configuration class and needs to be registered with the addInterceptors helper method.

package com.apress.springrecipes.mobile.web.config;
import org.springframework.mobile.device.DeviceResolverHandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration extends WebMvcConfigurerAdapter {
...
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DeviceResolverHandlerInterceptor());
    }
}

The MobileConfiguration class extends WebMvcConfigurerAdapter from this class, and you can override the addInterceptors method. All the interceptors added to the registry will be added to the HandlerMapping beans in the application context. When the application is deployed and a request is made to http://localhost:8080/mobile/home, the result should be the same as for the filter.

8-3. Use Site Preferences

Problem

You want to allow users to choose which type of site they visit with their device and store this for future reference.

Solution

Use the SitePreference support provided by Spring Mobile.

How It Works

Both SitePreferenceRequestFilter and SitePreferenceHandlerInterceptor delegate retrieval of the current SitePreference to a SitePreferenceHandler object. The default implementation uses a SitePreferenceRepository class to store the preferences; by default this is done in a cookie.

Use SitePreferenceRequestFilter

Using SitePreferenceRequestFilter is a matter of adding it to the web application and mapping it to the servlet or requests that you want it to handle. For your application, that means adding it to the getServletFilters method. The advantage of using a filter is that it is possible to use it even outside a Spring-based application. It could also be used in a JSF-based application.

package com.apress.springrecipes.mobile.web;

import org.springframework.mobile.device.site.SitePreferenceRequestFilter;
...


public class MobileApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new DeviceResolverRequestFilter(),
            new SitePreferenceRequestFilter()};
    }


...
}

Now that SitePreferenceRequestFilter is registered, it will inspect incoming requests. If a request has a parameter named site_preference, it will use the passed-in value (NORMAL, MOBILE, or TABLET) to set the SitePreference value. The determined value is stored in a cookie and used for future reference; if a new value is detected, the cookie value will be reset. Modify the home.jsp pages to include the following in order to display the current SitePreference value:

<p>
    Your site preferences <c:out value="${requestScope.currentSitePreference}" />
</p>

Now opening the page using the URL http://localhost:8080/mobile/home?site_preference=TABLET will set the SitePreference value to TABLET.

Use SitePreferenceHandlerInterceptor

When using Spring Mobile in a Spring MVC–based application, it is easier to work with SitePreferenceHandlerInterceptor . This needs to be configured in your configuration class and needs to be registered with the addInterceptors helper method.

package com.apress.springrecipes.mobile.web.config;

import org.springframework.mobile.device.DeviceResolverHandlerInterceptor;
import org.springframework.mobile.device.site. SitePreferenceHandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;


@Configuration
@EnableWebMvc
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration extends WebMvcConfigurerAdapter {
...
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DeviceResolverHandlerInterceptor());
        registry.addInterceptor(new SitePreferenceHandlerInterceptor());
    }
}

The MobileConfiguration class extends WebMvcConfigurerAdapter from this class, and you can override the addInterceptors method. All the interceptors added to the registry will be added to the HandlerMapping beans in the application context. When the application is deployed and a request is made to http://localhost:8080/mobile/home?site_preference=TABLET, the result should be the same as for the filter in the previous section.

8-4. Use the Device Information to Render Views

Problem

You want to render a different view based on the device or site preferences .

Solution

Use the current Device and SitePreferences objects to determine which view to render. This can be done manually or by using LiteDeviceDelegatingViewResolver.

How It Works

Now that the type of device is known , it can be used to your advantage. First let’s create some additional views for each type of device supported and put them, respectively , in a mobile or tablet directory under WEB-INF/views. Here’s the source for mobile/home.jsp:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!doctype html>
<html>
<body>


<h1>Welcome Mobile User</h1>
<p>
    Your User-Agent header: <c:out value="${header['User-Agent']}" />
</p>
<p>
    Your type of device: <c:out value="${requestScope.currentDevice}" />
</p>
<p>
    Your site preferences <c:out value="${requestScope.currentSitePreference}" />
</p>
</body>
</html>

Here’s the source for tablet/home.jsp:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!doctype html>
<html>
<body>


<h1>Welcome Tablet User</h1>
<p>
    Your User-Agent header: <c:out value="${header['User-Agent']}" />
</p>
<p>
    Your type of device: <c:out value="${requestScope.currentDevice}" />
</p>
<p>
    Your site preferences <c:out value="${requestScope.currentSitePreference}" />
</p>
</body>
</html>

Now that the different views are in place, you need to find a way to render them based on the device value that has been detected. One way would be to manually get access to the current device from the request and use that to determine which view to render.

package com.apress.springrecipes.mobile.web;

import org.springframework.mobile.device.Device;
import org.springframework.mobile.device.DeviceUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


import javax.servlet.http.HttpServletRequest;

@Controller
public class HomeController {


    @RequestMapping("/home")
    public String index(HttpServletRequest request) {
        Device device = DeviceUtils.getCurrentDevice(request);
        if (device.isMobile()) {
            return "mobile/home";
        } else if (device.isTablet()) {
            return "tablet/home";
        } else {
            return "home";
        }
    }
}

Spring Mobile has a DeviceUtils class that can be used to retrieve the current device. The current device is retrieved from a request attribute (currentDevice), which has been set by the filter or interceptor. The device value can be used to determine which view to render.

Getting the device in each method that needs it isn’t very convenient. It would be a lot easier if it could be passed into the controller method as a method argument. For this, you can use DeviceHandlerMethodArgumentResolver, which can be registered and will resolve the method argument to the current device. To retrieve the current SitePreference value, you can add a SitePreferenceHandlerMethodArgumentResolver class.

package com.apress.springrecipes.mobile.web.config;

import org.springframework.mobile.device.DeviceHandlerMethodArgumentResolver;
...
import java.util.List;


@Configuration
@EnableWebMvc
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration extends WebMvcConfigurerAdapter {


...
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new DeviceHandlerMethodArgumentResolver());
        argumentResolvers.add(new SitePreferenceHandlerMethodArgumentResolver());
    }
}

Now that these have been registered , the controller method can be simplified, and the Device value can be passed in as a method argument.

package com.apress.springrecipes.mobile.web;

import org.springframework.mobile.device.Device;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


import javax.servlet.http.HttpServletRequest;

@Controller
public class HomeController {


    @RequestMapping("/home")
    public String index(Device device) {
        if (device.isMobile()) {
            return "mobile/home";
        } else if (device.isTablet()) {
            return "tablet/home";
        } else {
            return "home";
        }
    }
}

The method signature changed from having an HttpServletRequest value to a Device value. That takes care of the lookup and will pass in the current device . However, while this is more convenient than manual retrieval, it is still quite an antiquated way to determine which view to render. Currently, the preferences aren’t taken into account, but this could be added to the method signature and could be used to determine the preference. However, it would complicate the detection algorithm. Imagine this code in multiple controller methods, which would soon become a maintenance nightmare.

Spring Mobile ships with a LiteDeviceDelegatingViewResolver class, which can be used to add additional prefixes and/or suffixes to the view name, before it is passed to the actual view resolver. It also takes into account the optional site preferences of the user.

package com.apress.springrecipes.mobile.web.config;

import org.springframework.mobile.device.view.LiteDeviceDelegatingViewResolver;
...


@Configuration
@EnableWebMvc
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration extends WebMvcConfigurerAdapter {
...
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/views/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setOrder(2);
        return viewResolver;
    }


    @Bean
    public ViewResolver mobileViewResolver() {
        LiteDeviceDelegatingViewResolver delegatingViewResolver =
            new LiteDeviceDelegatingViewResolver(viewResolver());
        delegatingViewResolver.setOrder(1);
        delegatingViewResolver.setMobilePrefix("mobile/");
        delegatingViewResolver.setTabletPrefix("tablet/");
        return delegatingViewResolver;


    }
}

LiteDeviceDelegatingViewResolver takes a delegate view resolver as a constructor argument; the earlier configured InternalResourceViewResolver is passed in as the delegate. Also, note the ordering of the view resolvers; you have to make sure that LiteDeviceDelegatingViewResolver executes before any other view resolver. This way, it has a chance to determine whether a custom view for a particular device exists. Next, notice in the configuration that the views for mobile devices are located in the mobile directory, and for the tablet they are in the tablet directory. To add these directories to the view names, the prefixes for those device types are set to their respective directories. Now when a controller returns home as the view name to select for a mobile device, it would be turned into mobile/home. This modified name is passed on to InternalResourceViewResolver, which turns it into /WEB-INF/views/mobile/home.jsp, the page you actually want to render.

package com.apress.springrecipes.mobile.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;


@Controller
public class HomeController {


    @RequestMapping("/home")
    public String index() {
        return "home";
    }
}

The controller is quite clean now. Its only concern is to return the name of the view. The determination of which view to render is left to the configured view resolvers. LiteDeviceDelegatingViewResolver takes into account any SitePreferences values when found.

8-5. Implement Site Switching

Problem

Your mobile site is hosted on a different URL than your normal web site.

Solution

Use Spring Mobile’s site switching support to redirect to the appropriate part of your web site.

How It Works

Spring Mobile comes with a SiteSwitcherHandlerInterceptor class, which you can use to switch to a mobile version of your site based on the detected Device value. To configure SiteSwitcherHandlerInterceptor, there are a couple of factory methods that provide ready-to-use settings (see Table 8-1).

Table 8-1. Overview of Factory Methods on SiteSwitcherHandlerInterceptor

Factory Method

Description

mDot

Redirects to a domain starting with m.; for instance, http://www.yourdomain.com would redirect to http://m.yourdomain.com .

dotMobi

Redirects to a domain ending with .mobi. A request to http://www.yourdomain.com would redirect to http://www.yourdomain.mobi .

urlPath

Sets up different context roots for different devices. This will redirect to the configured URL path for that device. For instance, http://www.yourdomain.com could be redirected to http://www.yourdomain.com/mobile .

standard

This is the most flexible configurable factory method, which allows you to specify a domain to redirect to for the mobile, tablet, and normal versions of your web site.

SiteSwitcherHandlerInterceptor also provides the ability to use site preferences. When using SiteSwitcherHandlerInterceptor, you don’t need to register SitePreferencesHandlerInterceptor anymore because this is already taken care of. Configuration is as simple as adding it to the list of interceptors you want to apply; the only thing to remember is that you need to place it after DeviceResolverHandlerInterceptor because the device information is needed to calculate the redirection URL.

package com.apress.springrecipes.mobile.web.config;

...
import org.springframework.mobile.device.switcher.SiteSwitcherHandlerInterceptor;


@Configuration
@EnableWebMvc
@ComponentScan("com.apress.springrecipes.mobile.web")
public class MobileConfiguration extends WebMvcConfigurerAdapter {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new DeviceResolverHandlerInterceptor());
        registry.addInterceptor(siteSwitcherHandlerInterceptor());
    }


    @Bean
    public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
        return SiteSwitcherHandlerInterceptor.mDot("yourdomain.com", true);
    }
...
}

Notice in the bean declaration for SiteSwitcherHandlerInterceptor that the factory method mDot is used to create an instance. The method takes two arguments. The first is the base domain name to use, and the second is a boolean indicating whether tablets should be considered mobile devices. The default is false. This configuration would lead to redirecting a request to the normal web site from mobile devices to m.yourdomain.com.

@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.dotMobi("yourdomain.com", true);
}

The previous configuration uses the dotMobi factory method , which takes two arguments. The first is the base domain name to use, and the second is a Boolean indicating whether tablets are to be considered mobile devices; the default is false. This would lead to redirecting requests to your normal web site from mobile devices to yourdomain.mobi.

@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.urlPath("/mobile", "/tablet", "/home");
}

The previous configuration uses the urlPath factory method with three arguments. The first argument is the context root for mobile devices, and the second is the context root for tablets. The final argument is the root path or your application. There are two more variations of the urlPath factory method: one that takes only the path for mobile devices and another that takes a path for mobile devices and a root path. The previous configuration will lead to requests from mobile devices being redirected to yourdomain.com/home/mobile and for tablets to yourdomain.com/home/tablet.

Finally, there is the standard factory method, which is the most flexible and elaborate to configure.

@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor
        .standard("yourdomain.com", "mobile.yourdomain.com",
            "tablet.yourdomain.com", "*.yourdomain.com");
}

The previous configuration uses the standard factory method. It specifies a different domain for the normal, mobile, and tablet versions of the web site. Finally, it specifies the domain name of the cookie to use for storing the site preferences. This is needed because of the different subdomains specified.

There are several other variations of the standard factory method that allow for a subset of configuration of what was shown earlier.

Summary

In this chapter, you learned how to use Spring Mobile, which can detect the device that is requesting a page and can allow the user to select a certain page based on preferences. You learned how you can detect a user’s device using DeviceResolverRequestFilter or DeviceResolverHandlerInterceptor. You also learned how you can use SitePreferences to allow the user to override the detected device. Next, you looked at how you can use the device information and preferences to render a view for that device. Finally, you learned how to redirect the user to a different part of your web site based on the user’s device or site preferences.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset