About Rafal Borowiec

Rafal is an IT specialist with about 8 years of commercial experience, specializing in software testing and quality assurance, software development, project management and team leadership.

Thymeleaf template layouts in Spring MVC application with no extensions

After some years with JSP/JSTL and Apache Tiles I started discovering Thymeleaf for my Spring MVC applications. Thymeleaf is a really great view engine and it simplifies and speeds up the development despite that lack of good IntelliJ (vote here: http://youtrack.jetbrains.com/issue/IDEABKL-6713) support at the moment (there is an Eclipse plugin though). While learning how to use Thymeleaf I investigated different possibilities of working with layouts.

Apart from the native fragment inclusion mechanism there are at least two options to work with layouts: Thymeleaf integration with Apache Tile and Thymeleaf Layout Dialect. Both seem to work fine, but inspired by this comment about a simple and custom option, I gave it a try. In this post I will show I created the solution.

Create a Spring MVC project with Thymeleaf support

To get started quickly I used my Spring MVC Archetype with Thymeleaf 2.1 support. I created a project by simply invoking the archetype and then imported it to IntellJ.

Creating the layout file

In WEB-INF/views directory I created a layouts folder where I placed the my first layout file called default.html: The ${view} variable will contain the view name returned by the @Controller and the content fragment from ${view} file will be placed here.

Creating the view file

I edited WEB-INF/views/homeNotSignedIn.html and I defined the content fragment like this: So the only change was defining the fragment named content and removing duplicated fragment inclusions. No additional changes are required. The @Controller returns the original view name, as it was before:

@Controller
class HomeController {

 @RequestMapping(value = "/", method = RequestMethod.GET)
 String index(Principal principal) {
  return principal != null ? "home/homeSignedIn" : "home/homeNotSignedIn";
 }
}

I changed other views accordingly.

Creating the interceptor and integrating with Spring MVC

To finish the “new layout framework” I created a handler interceptor that will do the work:

public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter {

    private static final String DEFAULT_LAYOUT = "layouts/default";
    private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (!modelAndView.hasView()) {
            return;
        }
        String originalViewName = modelAndView.getViewName();
        modelAndView.setViewName(DEFAULT_LAYOUT);
        modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName);
    }    
}

ThymeleafLayoutInterceptor gets the original view name returned from the handler’s method and replaces it with the layout name (that is defined in WEB-INF/views/layouts/default.html). The original view is placed in the model as a view variable, so it can be used in the layout file. I overrode the postHandle method, as it is executed just before rendering the view.

Adding the interceptor is easy:

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ThymeleafLayoutInterceptor());
    }
}

And that’s it for the basic configuration. Not a rocket since. The result after going to localhost:8080. This is what I expected. Works like a charm. So I try to signup for an account and what I see after submitting a form:

500 returned for /signup with message Error resolving template "redirect:/", template might not exist or might not be accessible by any of the configured Template Resolvers

Of course,
redirect:/ after the form submission. I needed to modify the interceptor like this:

public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter {

    private static final String DEFAULT_LAYOUT = "layouts/default";
    private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (!modelAndView.hasView()) {
            return;
        }
        String originalViewName = modelAndView.getViewName();
        if (isRedirectOrForward(originalViewName)) {
            return;
        }
        modelAndView.setViewName(DEFAULT_LAYOUT);
        modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName);
    } 
    private boolean isRedirectOrForward(String viewName) {
        return viewName.startsWith("redirect:") || viewName.startsWith("forward:");
    }   
}

And it worked as expected. But I realized that I need to define and additional layout because Signup and Signin used this before, but not after applying the above changes.

Creating additional layouts

I created a new layout called blank.html and placed it to WEB-INF/views/layouts folder. But how to use select the layout? Probably there are many ways to do this. One of the easiest I though is to return the layout name from the @Controller by simply adding a model attribute named layout. If no layout is given, default one is used, otherwise the given one. Simple. But I wanted a more robust solution. So I thought maybe an annotation that I could use like this:

@Controller
class SigninController {

    @Layout(value = "layouts/blank")
    @RequestMapping(value = "signin")
    String signin() {
        return "signin/signin";
    }
}

To me it sounded like a good solution. So I implemented it.

Selecting the layout

I created a method level @Layout annotation that I placed in org.thymeleaf.spring.support package (together with ThymeleafLayoutInterceptor):

package org.thymeleaf.spring.support;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Layout {
    String value() default "";
}

I changed the interceptor as follows:

public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter {

    private static final String DEFAULT_LAYOUT = "layouts/default";
    private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (!modelAndView.hasView()) {
            return;
        }
        String originalViewName = modelAndView.getViewName();
        if (isRedirectOrForward(originalViewName)) {
            return;
        }
        String layoutName = getLayoutName(handler);
        modelAndView.setViewName(layoutName);
        modelAndView.addObject(DEFAULT_VIEW_ATTRIBUTE_NAME, originalViewName);
    }

    private boolean isRedirectOrForward(String viewName) {
        return viewName.startsWith("redirect:") || viewName.startsWith("forward:");
    }

    private String getLayoutName(Object handler) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Layout layout = handlerMethod.getMethodAnnotation(Layout.class);
        if (layout == null) {
            return DEFAULT_LAYOUT;
        } else {
            return layout.value();
        }
    }
}

Now, when the handler method is annotated with @Layout annotation, it get its value attribute. Works great. But when I started to change SignupController I realized I need to annotate both methods. It would be better if my annotation can be used for all methods at once, by annotating the @Controller class:

@Controller
@Layout(value = "layouts/blank")
class SignupController {

}

So I did.

Final touches

Firstly, I changed the annotation so it can be targeted at the type level:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Layout {
    String value() default "";
}

And the interceptor:

public class ThymeleafLayoutInterceptor extends HandlerInterceptorAdapter {

    private static final String DEFAULT_LAYOUT = "layouts/default";
    private static final String DEFAULT_VIEW_ATTRIBUTE_NAME = "view";

    private String defaultLayout = DEFAULT_LAYOUT;
    private String viewAttributeName = DEFAULT_VIEW_ATTRIBUTE_NAME;

    public void setDefaultLayout(String defaultLayout) {
        Assert.hasLength(defaultLayout);
        this.defaultLayout = defaultLayout;
    }

    public void setViewAttributeName(String viewAttributeName) {
        Assert.hasLength(defaultLayout);
        this.viewAttributeName = viewAttributeName;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        if (!modelAndView.hasView()) {
            return;
        }
        String originalViewName = modelAndView.getViewName();
        if (isRedirectOrForward(originalViewName)) {
            return;
        }
        String layoutName = getLayoutName(handler);
        modelAndView.setViewName(layoutName);
        modelAndView.addObject(this.viewAttributeName, originalViewName);
    }

    private boolean isRedirectOrForward(String viewName) {
        return viewName.startsWith("redirect:") || viewName.startsWith("forward:");
    }

    private String getLayoutName(Object handler) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Layout layout = getMethodOrTypeAnnotation(handlerMethod);
        if (layout == null) {
            return this.defaultLayout;
        } else {
            return layout.value();
        }
    }

    private Layout getMethodOrTypeAnnotation(HandlerMethod handlerMethod) {
        Layout layout = handlerMethod.getMethodAnnotation(Layout.class);
        if (layout == null) {
            return handlerMethod.getBeanType().getAnnotation(Layout.class);
        }
        return layout;
    }
}

As you can see method level annotation is more important than type level annotation, which gives some flexibility. In addition, I added a possibility to configure the interceptor. I thought, that setting the default layout name and view attribute name may be useful.

Summary

The presented solution may need some polishing in order to use it in production, but it shows how simply we can build template layouts without adding extra libraries to our project and utilizing only core Thymeleaf features. Please share you comments and opinions about the solution.

 

Related Whitepaper:

Functional Programming in Java: Harnessing the Power of Java 8 Lambda Expressions

Get ready to program in a whole new way!

Functional Programming in Java will help you quickly get on top of the new, essential Java 8 language features and the functional style that will change and improve your code. This short, targeted book will help you make the paradigm shift from the old imperative way to a less error-prone, more elegant, and concise coding style that’s also a breeze to parallelize. You’ll explore the syntax and semantics of lambda expressions, method and constructor references, and functional interfaces. You’ll design and write applications better using the new standards in Java 8 and the JDK.

Get it Now!  

Leave a Reply


1 × = seven



Java Code Geeks and all content copyright © 2010-2014, Exelixis Media Ltd | Terms of Use | Privacy Policy
All trademarks and registered trademarks appearing on Java Code Geeks are the property of their respective owners.
Java is a trademark or registered trademark of Oracle Corporation in the United States and other countries.
Java Code Geeks is not connected to Oracle Corporation and is not sponsored by Oracle Corporation.

Sign up for our Newsletter

20,709 insiders are already enjoying weekly updates and complimentary whitepapers! Join them now to gain exclusive access to the latest news in the Java world, as well as insights about Android, Scala, Groovy and other related technologies.

As an extra bonus, by joining you will get our brand new e-books, published by Java Code Geeks and their JCG partners for your reading pleasure! Enter your info and stay on top of things,

  • Fresh trends
  • Cases and examples
  • Research and insights
  • Two complimentary e-books