Scalate integration in Spring Boot
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-tomcatartifact which includes neccessary tomcat libraries - We have to add the
scalatedependency, 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 (
libandclasses) - we will use themaven-antrun-pluginto 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/webappfolder 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
Hinweis: Nur ein Mitglied dieses Blogs kann Kommentare posten.