Scalate integration in Spring Boot

Keine Kommentare
MongoDB

Developing the Surpreso frontend in Rails I really liked to write my views with the HAML template engine. Investing more time with Spring Boot I searched for a Java pendant and got aware of Scalate, the scala template engine.

At this time Scalate supports 4 template formats:
  • Mustache
  • SSP
  • Scaml (which is equal to HAML)
  • Jade (which is little bit nicer than HAML :-)

Integrating it in Spring Boot seemed very easy and I wrote a small Git pull-request with passing tests. Afterwards I wanted to write this small tutorial to do the steps manually and found some ugly packaging hooks. The post shows how to pass them and I hope to correct the pull-request in the next days.
As always you can find the source code for this project on GitHub.

Maven configuration

In opposite to the Spring Boot defaults I store my template files under src/main/webapp/WEB-INFO/templates. If you work with Eclipse it won't reload the complete server project when you change your views. But this needs some management in the pom.xml to get the tests run.

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemalocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

 <modelversion>4.0.0</modelversion>

 <groupid>cnagel</groupid>
 <artifactid>spring-scalate</artifactid>
 <version>0.0.1-SNAPSHOT</version>

 <!-- The packaging format, use jar for standalone projects -->
 <packaging>war</packaging>

 <properties>
  <!-- The main class to start by executing java -jar -->
  <start-class>cnagel.spring_scalate.Application</start-class>
  <project .build.sourceencoding="">UTF-8</project>
  <project .reporting.outputencoding="">UTF-8</project>
  <scala .version="">2.10.0</scala>
  <scalate .version="">1.6.1</scalate>
 </properties>

 <!-- Inherit defaults from Spring Boot -->
 <parent>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-parent</artifactid>
  <version>1.0.0.RC4</version>
 </parent>

 <dependencies>
  <!-- Spring Boot -->
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-web</artifactid>
  </dependency>
  <!-- Spring Boot Test -->
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-test</artifactid>
   <scope>test</scope>
  </dependency>
  <!-- Spring Tomcat libs needed for Eclipse -->
  <dependency>
   <groupid>org.springframework.boot</groupid>
   <artifactid>spring-boot-starter-tomcat</artifactid>
   <scope>provided</scope>
  </dependency>
  <!-- Scalate -->
  <dependency>
   <groupid>org.fusesource.scalate</groupid>
   <artifactid>scalate-spring-mvc_2.10</artifactid>
   <version>${scalate.version}</version>
  </dependency>
 </dependencies>

 <build>
  <plugins>
   <!-- Use plugin to package as an executable JAR -->
   <plugin>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-maven-plugin</artifactid>
   </plugin>
   <!-- Scalate template pre compiler -->
   <plugin>
    <groupid>org.fusesource.scalate</groupid>
    <artifactid>maven-scalate-plugin_2.10</artifactid>
    <version>${scalate.version}</version>
    <executions>
     <execution>
      <goals>
       <goal>precompile</goal>
      </goals>
     </execution>
    </executions>
   </plugin>
   <!-- Create classes and lib folder in test phase for Scalate -->
   <plugin>
    <groupid>org.apache.maven.plugins</groupid>
    <artifactid>maven-antrun-plugin</artifactid>
    <executions>
     <execution>
      <id>test-compile</id>
      <phase>test-compile</phase>
      <goals>
       <goal>run</goal>
      </goals>
      <configuration>
       <tasks>
        <mkdir dir="target/test-classes/WEB-INF/classes">
        <mkdir dir="target/test-classes/WEB-INF/lib">
       </mkdir></mkdir></tasks>
      </configuration>
     </execution>
    </executions>
   </plugin>
  </plugins>
  <pluginmanagement>
   <plugins>
    <!-- Executes the antrun plugin in Eclipse too -->
    <plugin>
     <groupid>org.eclipse.m2e</groupid>
     <artifactid>lifecycle-mapping</artifactid>
     <version>1.0.0</version>
     <configuration>
      <lifecyclemappingmetadata>
       <pluginexecutions>
        <pluginexecution>
         <pluginexecutionfilter>
          <groupid>org.apache.maven.plugins</groupid>
          <artifactid>maven-antrun-plugin</artifactid>
          <versionrange>1.7</versionrange>
          <goals>
           <goal>run</goal>
          </goals>
         </pluginexecutionfilter>
         <action>
          <execute>
         </execute></action>
        </pluginexecution>
       </pluginexecutions>
      </lifecyclemappingmetadata>
     </configuration>
    </plugin>
   </plugins>
  </pluginmanagement>
  <!-- Adds the WEB-INF folder to the test-classes directory to include templates -->
  <testresources>
   <testresource>
    <directory>src/main/webapp</directory>
   </testresource>
  </testresources>
 </build>

 <!-- Allow access to Spring milestones -->
 <repositories>
  <repository>
   <id>spring-milestones</id>
   <name>Spring Milestones</name>
   <url>http://repo.spring.io/milestone</url>
   <snapshots>
    <enabled>false</enabled>
   </snapshots>
  </repository>
 </repositories>
 <pluginrepositories>
  <pluginrepository>
   <id>spring-milestones</id>
   <url>http://repo.spring.io/milestone</url>
  </pluginrepository>
 </pluginrepositories>

</project>

OK, this one looks big, lets go through the interesting parts:

  • Packaging format is war to deploy the project in the STS or Eclipse server
  • Therefore we have to add a dependency to the spring-boot-starter-tomcat artifact which includes neccessary tomcat libraries
  • We have to add the scalate dependency, fortunately there exists a Spring MVC artifact
  • Scalate has a nice feature to precompile your templates in the maven package cycle and we only have to include the maven-scalate-plugin_2.10
  • Executing the tests Scalate logs some ugly warnings with exception stacktraces for missing folders (lib and classes) - we will use the maven-antrun-plugin to create the temporary directories in the test phase
  • Eclipse ignores this plugin and therefore we have to force the execution in the IDE too
  • Last we have to put the src/main/webapp folder to our test resources. Otherwise it is not possible to check the output of the templates in the tests

Spring configuration

For simplicity I put the Spring Boot configuration into the Application class.

package cnagel.spring_scalate;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@ComponentScan("cnagel.spring_scalate")
public class Application {

 public static void main(String... args) {
  SpringApplication.run(Application.class, args);
 }

}

Additionally it needs a Servlet initializer to run the application in a server like Tomcat or Jetty.

package cnagel.spring_scalate;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;

public class WebXml extends SpringBootServletInitializer {

 @Override
 protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
  return application.sources(Application.class);
 }

}

Being the default bootstrapping process it now needs a Scalate configuration to load the template engine.

package cnagel.spring_scalate.config;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.ServletContext;
import org.fusesource.scalate.layout.DefaultLayoutStrategy;
import org.fusesource.scalate.servlet.Config;
import org.fusesource.scalate.servlet.ServletTemplateEngine;
import org.fusesource.scalate.spring.view.ScalateViewResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.context.ServletContextAware;
import scala.collection.JavaConversions;

@Configuration
public class ScalateConfig {

 public static final String DEFAULT_PREFIX = "/WEB-INF/templates/";
 public static final String DEFAULT_SUFFIX = ".jade";
 public static final String DEFAULT_LAYOUT = "/WEB-INF/templates/layouts/default.jade";

 @Configuration
 protected static class ScalateConfigConfiguration implements ServletContextAware {

  private ServletContext servletContext;

  @Override
  public void setServletContext(ServletContext servletContext) {
   this.servletContext = servletContext;
  }

  @Bean
  public Config config() {
   return new Config() {

    @Override
    public ServletContext getServletContext() {
     return servletContext;
    }

    @Override
    public String getName() {
     return "unknown";
    }

    @Override
    public Enumeration getInitParameterNames() {
     return null;
    }

    @Override
    public String getInitParameter(String name) {
     return null;
    }
   };
  }

 }

 @Configuration
 protected static class ScalateServletTemplateEngineConfiguration {

  @Autowired
  private Config config;

  @Bean
  public ServletTemplateEngine servletTemplateEngine() {
   ServletTemplateEngine engine = new ServletTemplateEngine(config);
   List layouts = new ArrayList(1);
   layouts.add(DEFAULT_LAYOUT);
   engine.layoutStrategy_$eq(new DefaultLayoutStrategy(engine,
     JavaConversions.asScalaBuffer(layouts)));
   return engine;
  }
 }

 @Configuration
 protected static class ScalateViewResolverConfiguration {

  @Autowired
  private ServletTemplateEngine servletTemplateEngine;

  @Bean
  public ScalateViewResolver scalateViewResolver() {
   ScalateViewResolver resolver = new ScalateViewResolver();
   resolver.templateEngine_$eq(servletTemplateEngine);
   resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 20);
   resolver.setPrefix(DEFAULT_PREFIX);
   resolver.setSuffix(DEFAULT_SUFFIX);
   return resolver;
  }

 }

}

In a short version, the constants in the beginning of the class set the template folder, template extension, and the default layout. Furthermore it registers the ScalateViewResolver to render the views.

Controller

To show the functionality of Scalate we will only implement a very basic controller. This one renders an index site and offers an ajax endpoint to add form inputs to a list.

package cnagel.spring_scalate.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/")
public class HomeController {

 @RequestMapping(method = RequestMethod.GET)
 public ModelAndView index() {
  ModelAndView mav = new ModelAndView("layout:home/index");
  return mav;
 }

 @RequestMapping(method = RequestMethod.POST)
 public ModelAndView post(@RequestParam("text") String text) {
  ModelAndView mav = new ModelAndView("home/post");
  mav.addObject("text", text);
  return mav;
 }

}

In the index action we only define to render the home/index template. Given the default folder and extension in the configuration before, Scalate will use the src/main/webapp/WEB-INF/templates/home/index.jade file to render the output. The layout: prefix instructs Scalate to render the layout too and embed the view.
The post action handles incoming POST requests. Being an ajax endpoint it just renders the home/post view without the layout and assigns the posted text param.

Templates

There exist one layout and two templates in the project. The layout loads Twitter Bootstrap for CSS and jQuery for Javascript to easy the ajax request. Have a look at it to see the cuteness of the Jade template format.
The index view simply renders a form with a text input. By submitting the form, an ajax request gets executed and the response appended to a list.
The post view only renders a list element with the text param as content.

Testing

The tests simply use the provided Spring MVC mocks and check if both action render the correct views and if needed the layout.

package cnagel.spring_scalate.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.internal.matchers.Contains;
import org.mockito.internal.matchers.StartsWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import cnagel.spring_scalate.Application;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration("classpath:.")
public class HomeControllerTests {

 @Autowired
 private WebApplicationContext ctx;

 private MockMvc mvc;

 @Before
 public void setUp() {
  mvc = MockMvcBuilders.webAppContextSetup(ctx).build();
 }

 @Test
 public void test_index() throws Exception {
  mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string(new Contains("<label>Say what ever:</label>"))).andExpect(content().string(new StartsWith("<!DOCTYPE html>")));
 }

 @Test
 public void test_post() throws Exception {
  mvc.perform(post("/").param("text", "Hello")).andExpect(status().isOk()).andExpect(content().string(new Contains("<li>Hello</li>")));
 }

}

Deployment

For development issues you can run the project in the server provided by Eclipse or STS. With mvn package it is possible to create a jar or war file by simply switching the package format in the pom.xml.
For me it was not possible to run the jar file under windows, having problems with illegal characters in the classpath.

Keine Kommentare :

Kommentar veröffentlichen