1. Overview

When designing a web application, its look-and-feel, or theme, is a key component. It impacts our application's usability and accessibility and can further establish our company's brand.

In this tutorial, we'll go through the steps required to configure themes in a Spring MVC application.

2. Use Cases

Simply put, themes are a set of static resources, typically stylesheets and images, that impact the visual style of our web application.

We can use themes to:

  • Establish a common look-and-feel with a fixed theme
  • Customize for a brand with a branding theme – this is common in a SAAS application where each client wants a different look-and-feel
  • Address accessibility concerns with a usability theme – for example, we might want a dark or a high-contrast theme

3. Maven Dependencies

So, first things first, let's add the Maven dependencies we'll be using for the first part of this tutorial.

We'll need the Spring WebMVC and Spring Context dependencies:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

And since we're going to use JSP in our example, we'll need Java Servlets, JSP, and JSTL:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
</dependency>
<dependency>
     <groupId>javax.servlet.jsp</groupId>
     <artifactId>javax.servlet.jsp-api</artifactId>
     <version>2.3.3</version>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

4. Configuring Spring Theme

4.1. Theme Properties

Now, let's configure light and dark themes for our application.

For the dark theme, let's create dark.properties:

styleSheet=themes/black.css
background=black

And for the light theme, light.properties:

styleSheet=themes/white.css
background=white

From the properties above, we notice that one refers to a CSS file and another refers to a CSS style. We'll see in a moment how these are manifest in our view.

4.2. ResourceHandler

Reading the properties above, the files black.css and white.css must be placed in the directory named /themes.

And, we must configure a ResourceHandler to enable Spring MVC to correctly locate the files when requested:

@Override 
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/themes/**").addResourceLocations("classpath:/themes/");
}

4.3. ThemeSource

We can manage these theme-specific .properties files as ResourceBundles via ResourceBundleThemeSource:

@Bean
public ResourceBundleThemeSource resourceBundleThemeSource() {
    return new ResourceBundleThemeSource();
}

4.4. ThemeResolvers

Next, we need a ThemeResolver to resolve the correct theme for the application. Depending on our design needs, we can choose between existing implementations or create our own.

For our example, let's configure the CookieThemeResolver. As the name depicts, this resolves the theme information from a browser cookie or falls back to the default if that information isn't available:

@Bean
public ThemeResolver themeResolver() {
    CookieThemeResolver themeResolver = new CookieThemeResolver();
    themeResolver.setDefaultThemeName("light");
    return themeResolver;
}

The other variants of ThemeResolver shipped with the framework are:

  • FixedThemeResolver: Used when there is a fixed theme for an application
  • SessionThemeResolver: Used to allow the user to switch themes for the active session

4.5. View

In order to apply the theme to our view, we must configure a mechanism to query the resource bundles.

We'll keep the scope to JSP only, though a similar lookup mechanism could be configured for alternate view rendering engines as well.

For JSPs, we can import a tag library that does the job for us:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

And then we can refer to any property specifying the appropriate property name:

<link rel="stylesheet" href="<spring:theme code='styleSheet'/>"/>

Or:

<body bgcolor="<spring:theme code='background'/>">

So, let's now add a single view called index.jsp into our application and place it in the WEB-INF/ directory:

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link rel="stylesheet" href="<spring:theme code='styleSheet'/>"/>
        <title>Themed Application</title>
    </head>
    <body>
        <header>
            <h1>Themed Application</h1>
            <hr />
        </header>
        <section>
            <h2>Spring MVC Theme Demo</h2>
            <form action="<c:url value='/'/>" method="POST" name="themeChangeForm" id="themeChangeForm">
                <div>
                    <h4>
                        Change Theme
                    </h4>
                </div>
                <select id="theme" name="theme" onChange="submitForm()">
                    <option value="">Reset</option>
                    <option value="light">Light</option>
                    <option value="dark">Dark</option>
                </select>
            </form>
        </section>

        <script type="text/javascript">
            function submitForm() {
                document.themeChangeForm.submit();
            }
        </script>
    </body>
</html>

Actually, our application would work at this point, always choosing our light theme.

Let's see how we can allow the user to change their theme.

4.6. ThemeChangeInterceptor

The job of the ThemeChangeInterceptor is to understand the theme change request.

Let's now add a ThemeChangeInterceptor and configure it to look for a theme request parameter:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(themeChangeInterceptor());
}

@Bean
public ThemeChangeInterceptor themeChangeInterceptor() {
    ThemeChangeInterceptor interceptor = new ThemeChangeInterceptor();
    interceptor.setParamName("theme");
    return interceptor;
}

5. Further Dependencies

Next, let's implement our own ThemeResolver that stores the user's preference to a database.

To achieve this, we'll need Spring Security for identifying the user:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.2.1.RELEASE</version>
</dependency>

And Spring Data, Hibernate, and HSQLDB for storing the user's preference:

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.4.9.Final</version>
</dependency>

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.5.0</version>
</dependency>

6. Custom ThemeResolver

Let's now dive more into ThemeResolver and implement one of our own. This custom ThemeResolver will save the user's theme preference to a database.

To achieve this, let's first add a UserPreference entity:

@Entity
@Table(name = "preferences")
public class UserPreference {
    @Id
    private String username;

    private String theme;
}

Next, we'll create UserPreferenceThemeResolver, which must implement the ThemeResolver interface. Its key responsibilities are to resolve and save theme information.

Let's first address resolving the name by implementing UserPreferenceThemeResolver#resolveThemeName:

@Override
public String resolveThemeName(HttpServletRequest request) {
    String themeName = findThemeFromRequest(request)
      .orElse(findUserPreferredTheme().orElse(getDefaultThemeName()));
    request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, themeName);
    return themeName;
}

private Optional<String> findUserPreferredTheme() {
    Authentication authentication = SecurityContextHolder.getContext()
            .getAuthentication();
    UserPreference userPreference = getUserPreference(authentication).orElse(new UserPreference());
    return Optional.ofNullable(userPreference.getTheme());
}

private Optional<String> findThemeFromRequest(HttpServletRequest request) {
    return Optional.ofNullable((String) request.getAttribute(THEME_REQUEST_ATTRIBUTE_NAME));
}
    
private Optional<UserPreference> getUserPreference(Authentication authentication) {
    return isAuthenticated(authentication) ? 
      userPreferenceRepository.findById(((User) authentication.getPrincipal()).getUsername()) : 
      Optional.empty();
}

And now we can write our implementation for saving the theme in UserPreferenceThemeResolver#setThemeName:

@Override
public void setThemeName(HttpServletRequest request, HttpServletResponse response, String theme) {
    Authentication authentication = SecurityContextHolder.getContext()
        .getAuthentication();
    if (isAuthenticated(authentication)) {
        request.setAttribute(THEME_REQUEST_ATTRIBUTE_NAME, theme);
        UserPreference userPreference = getUserPreference(authentication).orElse(new UserPreference());
        userPreference.setUsername(((User) authentication.getPrincipal()).getUsername());
        userPreference.setTheme(StringUtils.hasText(theme) ? theme : null);
        userPreferenceRepository.save(userPreference);
    }
}

And finally, let's now change out the ThemeResolver in our app:

@Bean 
public ThemeResolver themeResolver() { 
    return new UserPreferenceThemeResolver();
}

Now, the user's theme preference is saved in the database instead of as a cookie.

An alternative way of saving the user's preference could've been through a Spring MVC Controller and a separate API.

7. Conclusion

In this article, we learned the steps to configure Spring MVC themes.

We can also find the complete code over on GitHub.