© Adam L. Davis 2020
A. L. DavisSpring Quick Reference Guidehttps://doi.org/10.1007/978-1-4842-6144-6_8

8. Spring Mobile

Adam L. Davis1 
(1)
Oviedo, FL, USA
 

Spring Mobile is an extension to Spring MVC that aims to simplify the development of mobile web applications. It includes a module for detecting on the server the type of device, mobile, tablet, or desktop making a request.

Getting Started

Include the project in your dependencies, for example, in a Maven pom:
<dependency>
    <groupId>org.springframework.mobile</groupId>
    <artifactId>spring-mobile-device</artifactId>
    <version>${org.springframework.mobile-version}</version>
</dependency>
In a Gradle build file, add the following under dependencies:
implementation
  "org.springframework.mobile:spring-mobile-device:$mobileVersion"
Then set the version1 in your gradle.properties file:
mobileVersion=1.1.5.RELEASE

Next, add either the DeviceResolverHandlerInterceptor or DeviceResolverRequestFilter to your web application. The first is more tightly coupled to the Spring framework, while the second is an implementation of a servlet filter, so less coupled to Spring.

DeviceResolverHandlerInterceptor

Spring Mobile ships with a HandlerInterceptor that, on preHandle, delegates to a DeviceResolver . The resolved Device is set as a request attribute named “currentDevice”, making it available to handlers throughout the request processing.

To enable, add the DeviceResolverHandlerInterceptor to the list of interceptors defined in your DispatcherServlet configuration XML:
<interceptors>
  <bean class="org.springframework.mobile.device.DeviceResolverHandlerInterceptor" />
</interceptors>
Alternatively, you can add the DeviceResolverHandlerInterceptor using Spring's Java-based configuration:
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
//...
  @Bean
  public DeviceResolverHandlerInterceptor drhInterceptor() {
      return new DeviceResolverHandlerInterceptor();
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(drhInterceptor());
  }
}

DeviceResolverRequestFilter

As an alternative to the DeviceResolverHandlerInterceptor, Spring Mobile also ships with a servlet filter that delegates to a DeviceResolver. As with the HandlerInterceptor, the resolved Device is set under a request attribute named “currentDevice”.

To enable, add the DeviceResolverRequestFilter to your web.xml as follows:
<filter>
  <filter-name>deviceResolverRequestFilter</filter-name>
  <filter-class>
org.springframework.mobile.device.DeviceResolverRequestFilter
  </filter-class>
</filter>

Accessing the Device

To look up the current Device in your code, you can do so in several ways. If you already have a reference to a ServletRequest or Spring WebRequest, simply use DeviceUtils:
//imports
import org.springframework.mobile.device.DeviceUtils;
// code...
Device currentDevice = DeviceUtils.getCurrentDevice(servletRequest);

This would get the current device or null if no device has been resolved for the request. There’s also a getRequiredCurrentDevice(HttpServletRequest request) method that throws a runtime exception if the current device has not been resolved.

The Device interface has the following methods available:

Return Type

Method

DevicePlatform

getDevicePlatform() – Returns an enum which can be IOS, ANDROID, or UNKNOWN.

Boolean

isMobile() – True if this device is a mobile device such as an Apple iPhone or a Nexus One Android.

Boolean

isNormal() – True if this device is not a mobile or tablet device.

Boolean

isTablet() – True if this device is a tablet device such as an Apple iPad or a Motorola Xoom.

DeviceWebArgumentResolver

If you'd like to pass the current Device automatically as an argument to one or more of your controller methods, configure a DeviceWebArgumentResolver using XML:
<annotation-driven>
  <argument-resolvers>
    <bean class="org.springframework.mobile.device.DeviceWebArgumentResolver" />
  </argument-resolvers>
</annotation-driven>
You can alternatively configure a DeviceHandlerMethodArgumentResolver using a Java-based configuration like the following:
@Bean
public DeviceHandlerMethodArgumentResolver deviceHMAR() {
    return new DeviceHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
  argumentResolvers.add(deviceHMAR());
}

LiteDeviceResolver

Spring allows for different implementations of DeviceResolver, but by default provides an implementation which only detects the presence of a mobile or tablet device named LiteDeviceResolver.

You can also customize the LiteDeviceResolver by adding additional keywords that, if contained in a request’s User-Agent, will resolve as a “normal” device, for example, using Java configuration:
@Bean
public LiteDeviceResolver liteDeviceResolver() {
    List<String> keywords = new ArrayList<String>();
    keywords.add("vivaldi");
    keywords.add("yandex");
    return new LiteDeviceResolver(keywords);
}
@Bean
public DeviceResolverHandlerInterceptor deviceResolverHandlerInt() {
    return new DeviceResolverHandlerInterceptor(liteDeviceResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(deviceResolverHandlerInt());
}

Site Preference Management

Spring Mobile provides a single SitePreferenceHandler implementation named StandardSitePreferenceHandler, which should be suitable for most needs. It supports a query parameter–based site preference indication (site_preference) and pluggable SitePreference storage and may be enabled in a Spring MVC application using the SitePreferenceHandlerInterceptor. In addition, if a SitePreference has not been explicitly indicated by the user, a default will be derived based on the user’s Device detected.

So along with the previous interceptors, add the following:
    @Bean
    public SitePreferenceHandlerInterceptor
         sitePreferenceHandlerInterceptor() {
        return new SitePreferenceHandlerInterceptor();
    }
Then update the addInterceptors method to the following:
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(drhInterceptor());
        registry.addInterceptor(sitePreferenceHandlerInterceptor());
    }

Similarly to the Device resolution, you can gain access to the current SitePreference using either the SitePreferenceUtils or SitePreferenceHandlerMethodArgumentResolver. However, it might make more sense to redirect mobile users to a different site. In this case, you may use the SiteSwitcherHandlerInterceptor to redirect mobile users to a dedicated mobile site.

The mDot, dotMobi, urlPath, and standard factory methods of SitePreferenceHandlerInterceptor configure the cookie-based SitePreference storage. The cookie value will be shared across the mobile and normal site domains. Internally, the interceptor delegates to a SitePreferenceHandler, so there is no need to register a SitePreferenceHandlerInterceptor when using the switcher. For example, the following Interceptor would redirect mobile users to mobile.app.com, tablets to tablet.app.com, and otherwise just app.com:
@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.standard("app.com",
       "mobile.app.com", "tablet.app.com", ".app.com");
}
// standard(normalName, mobileServerName, tabletServerName, cookieDomain)
A simpler method that does not require additional DNS entries is the urlPath factory:
@Bean
public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor() {
    return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
}

This Interceptor would redirect mobile users to <your app>/mobile/ paths. For example, if the normal URL is “myapp.com/courses”, then the mobile site will be “myapp.com/mobile/courses”.

Spring Mobile Example

This example will build on the Spring Web MVC and Spring Data content from the previous chapters, as well as making use of Spring Boot. For more information on either topic, please see the related chapter. This example project is available online.2

To get started, create a new directory named “spring-mobile” and create a Gradle “build.gradle” file like the following:
plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id "java"
}
group = 'com.apress.spring-quick'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
ext {
    mobileVersion = '1.1.5.RELEASE'
}
repositories {
    mavenLocal()
    mavenCentral()
}
dependencies {
    implementation "org.springframework.boot:spring-boot-starter-actuator"
    implementation "org.springframework.boot:spring-boot-starter-web"
    implementation "org.springframework.boot:spring-boot-starter-groovy-templates"
    implementation "org.springframework.mobile:spring-mobile-device:$mobileVersion"
    implementation "com.apress.spring-quick:spring-data-jpa:0.0.1"
    implementation "com.h2database:h2:1.4.192" // database
}
Listing 8-1

build.gradle

This uses the Spring Boot Gradle plugin and dependency management to simplify setting up the project. Note that we include the “spring-data-jpa” project from Chapter 6 as a dependency. This makes the repositories available for potential inclusion as Spring beans at runtime (depending on the configuration).

Next, create a main class, like the following:
@SpringBootApplication
@Import({WebConfig.class, ServiceConfig.class})
public class SpringMobileWebApp {
    public static void main(String[] args) throws IOException {
        SpringApplication.run(SpringMobileWebApp.class, args);
    }
}
Listing 8-2

SpringMobileWebApp.java

Next, set up the ServiceConfig, which includes specific packages from the “spring-data-jpa” project from Chapter 6:
@Configuration
@EnableJpaRepositories(basePackages =
        {"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"},
        enableDefaultTransactions = true)
@ComponentScan(basePackages = {"com.apress.spring_quick.jpa.simple", "com.apress.spring_quick.jpa.compositions"})
public class ServiceConfig {
}
Next, we specify the WebConfig class, which defines Interceptors as described previously in this chapter, as well as GroovyMarkupConfigurer and GroovyMarkupViewResolver :
import org.springframework.context.annotation.*;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.mobile.device.*;
import org.springframework.mobile.device.site.*;
import org.springframework.mobile.device.switcher.*;
import org.springframework.web.method.support.*;
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public SitePreferenceHandlerMethodArgumentResolver sitePrefMAR() {
        return new SitePreferenceHandlerMethodArgumentResolver();
    }
    @Bean
    public DeviceHandlerMethodArgumentResolver deviceHMAR() {
        return new DeviceHandlerMethodArgumentResolver();
    }
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(deviceHMAR());
        argumentResolvers.add(sitePrefMAR());
    }
    @Bean
    public DeviceResolverHandlerInterceptor drhInterceptor() {
        return new DeviceResolverHandlerInterceptor();
    }
    @Bean
    public SitePreferenceHandlerInterceptor sitePreferenceHandlerInterceptor() {
        return new SitePreferenceHandlerInterceptor();
    }
    @Bean
    public SiteSwitcherHandlerInterceptor siteSwitcherHandlerInterceptor(){
        return SiteSwitcherHandlerInterceptor.urlPath("/mobile");
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(drhInterceptor());
        registry.addInterceptor(sitePreferenceHandlerInterceptor());
        registry.addInterceptor(siteSwitcherHandlerInterceptor());
    }
    @Bean
    public GroovyMarkupConfigurer groovyMarkupConfigurer() {
        GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
        configurer.setResourceLoaderPath("classpath:/templates/");
        return configurer;
    }
    @Bean
    public GroovyMarkupViewResolver groovyMarkupViewResolver() {
        GroovyMarkupViewResolver resolver = new GroovyMarkupViewResolver();
        resolver.setSuffix(".groovy");
        resolver.setRequestContextAttribute("requestContext");
        return resolver;
    }
}
Listing 8-3

WebConfig.java

Note that we’ve defined a SiteSwitcherHandlerInterceptor using the “/mobile” path. The Groovy-related configuration tells Spring to look under /templates/ in the classpath for files ending in “.groovy”.

Finally, we need to define the controllers for our MVC application. To enable a completely different logic for mobile sites, we can define a separate controller for mobile vs. normal requests. Alternatively, we could inject SitePreference as a method parameter to each controller method and use that instead since we set up a SitePreferenceHandlerMethodArgumentResolver.

In this case, we create a CourseController and MobileCourseController, which should look like the following:
@Controller
@RequestMapping
public class CourseController {
    @GetMapping("/")
    public String home() {
        return "home";
    }
    // additional methods...
Listing 8-4

CourseController.java

@Controller
@RequestMapping("/mobile")
public class MobileCourseController {
    @GetMapping("/")
    public String home() {
        return "mobile/home";
    }
    // additional methods...
Listing 8-5

MobileCourseController.java

Note that since the MobileCourseController is annotated with @RequestMapping("/mobile"), it will match all paths starting with “/mobile”, hence all users with the SitePreference of Mobile. Similarly, we could do the same for Tablet.

The Groovy markup templates should be placed under the src/main/resources/templates directory. The “home.groovy” template should look something like the following:
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
    head {
        meta('http-equiv':'"Content-Type" content: "text/html; charset: utf-8"')
        title('Courses Demo')
        link(rel: 'stylesheet', href: '/styles/main.css', type: 'text/css')
    }
    body {
        h3('Normal Home page')
        div(class: 'site_pref') {
            a(href: '/?site_preference=mobile', 'Mobile')
            yieldUnescaped '|'
            a(href: '/?site_preference=normal', 'Desktop')
        }
        div(class: 'content') {
            div {
                a(href: '/courses', 'Courses')
            }
        }
    }
}

Using the URL ?site_preference=mobile (or clicking the “Mobile” link on the web page which has the same URL path) triggers the SiteSwitcherHandlerInterceptor to change the SitePreference. In this case, the user would be redirected to the “mobile/home” view which is rendered by the file src/main/resources/templates/mobile/home.groovy.

../images/498572_1_En_8_Chapter/498572_1_En_8_Fig1_HTML.jpg
Figure 8-1

Mobile/normal home page

../images/498572_1_En_8_Chapter/498572_1_En_8_Figa_HTML.jpg Exercise: Add Tablets

Starting with the code from this chapter (which is available online3), add support for tablets.