Close

Spring MVC - Remembering User selected theme with CookieThemeResolver

[Updated: Jan 20, 2018, Created: Jan 19, 2018]

Some web application may want to provide options to the users to select a preferred theme rather then showing server side selected themes. CookieThemeResolver is another implementation of ThemeResolver (previously we have seen default FixedThemeResolver examples) that uses a cookie persisted on the client browser containing last selected theme name.

To understand how CookieThemeResolver works, let's see what methods the ThemeResolver interface has:

package org.springframework.web.servlet;
 ....
public interface ThemeResolver {
	/**
	      *  Resolve the current theme name for the given request.
	      *  The returned theme name is used by the ThemeSource implementation to
	      *  load theme properties file
	      */
	String resolveThemeName(HttpServletRequest request);

	/**
	      *  Set the current theme name for the given request and response
	      */
	void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response,
									@Nullable String themeName);

}

FixedThemeResolver#resolveThemeName() always returns a fixed theme name and its setThemeName() method throws UnsupportedOperationException, that means it does not support changing theme name dynamically.

CookieThemeResolver#resolveThemeName() looks for a HTTP request attribute by a particular name to find the user selected theme name if it is not found then it looks for a cookie to find the previously persisted theme selection there.

CookieThemeResolver#setThemeName() persists the cookie during runtime which contains the user selected theme name. This method should be called when user has selected a theme via a link or form etc.

We can use CookieThemeResolver in our controller to invoke above methods to achieve the desired results. The good news is Spring's ThemeChangeInterceptor provides this functionality out of the box. If interested check out the source code of ThemeChangeInterceptor#preHandle() method. This interceptor is not tied to CookieThemeResolver, instead it works with ThemeResolver instance, which means it can be used with others ThemeResolvers which want to change the themes dynamically during runtime.

Example

Java Config class

@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig implements WebMvcConfigurer {

  @Bean(DispatcherServlet.THEME_RESOLVER_BEAN_NAME)
  public ThemeResolver customThemeResolver() {
      CookieThemeResolver ctr = new CookieThemeResolver();
      ctr.setDefaultThemeName(ThemeInfo.DefaultThemeInfo.getThemeName());
      return ctr;
  }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
      ThemeChangeInterceptor themeChangeInterceptor = new ThemeChangeInterceptor();
      themeChangeInterceptor.setParamName("themeName");
      registry.addInterceptor(themeChangeInterceptor);
  }

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
      registry.jsp().prefix("/WEB-INF/views/").suffix(".jsp");
  }
}

Theme Properties files

src/main/resources/metal-theme.properties

background=hsla(0,0%,90%,1);  
content-style=width:500px;border:solid 2px grey;margin:auto;padding:30px;

src/main/resources/ocean-theme.properties

background=hsla(205, 89%, 25%, 0.3);
content-style=width:500px;border:solid 2px blue;margin:auto;padding:30px;

The Controller

In this example, we will allow user to select a theme from a dropdown 'option' component.

@Controller
public class ThemeController {

  @RequestMapping("/")
  public String getIndexPage(Model model, ThemeInfo themeInfo, HttpServletRequest request) {
      if (themeInfo.getThemeName() == null) {
          //this is needed for the situation when form first loaded via get method
          //so that the option component can select the last selected theme
          ThemeResolver themeResolver = RequestContextUtils.getThemeResolver(request);
          String themeName = themeResolver.resolveThemeName(request);
          themeInfo = ThemeInfo.getThemeInfoByName(themeName);
      }
      model.addAttribute("themeInfo", themeInfo);
      Map<String, String> themeChoices = ThemeInfo.getAllThemes()
                                                  .stream()
                                                  .collect(Collectors.toMap(
                                                          info -> info.getThemeName(),
                                                          info -> info.getDisplayName())
                                                  );
      //map's key/value equivalent to
      // html's <option value='key'>value</option>
      // used for theme selection
      model.addAttribute("themeChoices", themeChoices);
      return "index";
  }

  @RequestMapping("/otherView")
  public String getPage2() {
      return "other-page";
  }
}

The command object

public class ThemeInfo {
  public static final ThemeInfo DefaultThemeInfo =
          new ThemeInfo("metal-theme", "Metal Theme");
  private String themeName;
  private String displayName;

  public ThemeInfo(String themeName, String displayName) {
      this.themeName = themeName;
      this.displayName = displayName;
  }

  public static List<ThemeInfo> getAllThemes() {
      return Arrays.asList(
              new ThemeInfo("ocean-theme", "Ocean Theme"),
              DefaultThemeInfo
      );
  }

  public static ThemeInfo getThemeInfoByName(String themeName) {
      if (themeName == null) {
          return ThemeInfo.DefaultThemeInfo;
      }
      return getAllThemes().stream()
                           .filter(i -> i.getThemeName().equals(themeName))
                           .findAny().orElse(null);
  }

  public String getThemeName() {
      return themeName;
  }

  public void setThemeName(String name) {
      this.themeName = name;
  }

  public String getDisplayName() {
      return displayName;
  }

  public void setDisplayName(String displayName) {
      this.displayName = displayName;
  }
}

JSP Views:

/src/main/webapp/WEB-INF/views/index.jsp

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="fm"%>
<html>
<body style="background-color:<spring:theme code='background'/>;">

<fm:form method="post" action="/" modelAttribute="themeInfo">
    <fm:select path="themeName">
        <fm:options items="${themeChoices}"/>
    </fm:select>
    <input type="submit" value="Change Theme"/>
</fm:form>
<div style="<spring:theme code='content-style'/>">
    This is index page content
</div>
<a href="/otherView">Other View</a>

<br/>
</body>
</html>

/src/main/webapp/WEB-INF/views/other-page.jsp

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<body style="background-color:<spring:theme code='background'/>;">
<div style="<spring:theme code='content-style'/>">
    This is other page content
</div>
<a href="/">Home Page</a>
</body>
</html>

To try examples, run embedded tomcat (configured in pom.xml of example project below):

mvn tomcat7:run-war

Output

First access will select the default theme:

Changing and submitting the theme (by clicking 'Change Theme' button)

As seen in Chrome Developer Tools, the server sends the desired cookie in the response when the form is submitted.

Visiting 'Other View' link, the user selected theme is still remembered:

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 5.0.2.RELEASE: Spring Web MVC.
  • javax.servlet-api 3.1.0 Java Servlet API
  • JDK 1.8
  • Maven 3.3.9

CookieThemeResolver Example Select All Download
  • spring-cookie-theme-resolver
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • MyWebConfig.java
          • resources
          • webapp
            • WEB-INF
              • views

    See Also