Add Groovy template engine support for Spring MVC apps

Default suffix .tpl. If groovy-templates is on the classpath user
can now add templates and get them rendered and resolved in an MVC
app.

TODO: Macro helpers for message rendering etc.

See gh-878
This commit is contained in:
Dave Syer 2014-05-15 12:33:05 +01:00
parent 938609fdc0
commit da2e25f90d
17 changed files with 722 additions and 1 deletions

View File

@ -66,6 +66,11 @@
<artifactId>tomcat-jdbc</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-templates</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>

View File

@ -0,0 +1,135 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template;
import groovy.text.TemplateEngine;
import groovy.text.markup.BaseTemplate;
import groovy.text.markup.MarkupTemplateEngine;
import groovy.text.markup.TemplateConfiguration;
import java.net.URL;
import java.net.URLClassLoader;
import javax.annotation.PostConstruct;
import javax.servlet.Servlet;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.groovy.template.web.GroovyTemplateViewResolver;
import org.springframework.boot.autoconfigure.groovy.template.web.LocaleAwareTemplate;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.Ordered;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
/**
* Autoconfiguration support for Groovy templates in MVC. By default creates a
* {@link MarkupTemplateEngine} configured from {@link GroovyTemplateProperties}, but you
* can override that by providing a {@link TemplateEngine} of a different type.
*
* @author Dave Syer
* @since 1.1.0
*/
@Configuration
@ConditionalOnClass(TemplateEngine.class)
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(GroovyTemplateProperties.class)
public class GroovyTemplateAutoConfiguration {
@Autowired
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired
private GroovyTemplateProperties properties;
@PostConstruct
public void checkTemplateLocationExists() {
if (this.properties.isCheckTemplateLocation()) {
Resource resource = this.resourceLoader.getResource(this.properties
.getPrefix());
Assert.state(resource.exists(), "Cannot find template location: " + resource
+ " (please add some templates "
+ "or check your FreeMarker configuration)");
}
}
@Configuration
@ConditionalOnClass({ Servlet.class, LocaleContextHolder.class })
@ConditionalOnWebApplication
public static class GroovyWebConfiguration implements BeanClassLoaderAware {
@Autowired
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
@Autowired
private GroovyTemplateProperties properties;
private ClassLoader classLoader = GroovyWebConfiguration.class.getClassLoader();
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Bean
@ConditionalOnMissingBean(TemplateEngine.class)
public TemplateEngine groovyTemplateEngine() throws Exception {
TemplateConfiguration configuration = this.properties.getConfiguration();
if (configuration.getBaseTemplateClass() == BaseTemplate.class) {
// Enable locale-dependent includes
configuration.setBaseTemplateClass(LocaleAwareTemplate.class);
}
return new MarkupTemplateEngine(createParentLoaderForTemplates(),
configuration);
}
private ClassLoader createParentLoaderForTemplates() throws Exception {
return new URLClassLoader(new URL[] { this.resourceLoader.getResource(
this.properties.getPrefix()).getURL() }, this.classLoader);
}
@Bean
@ConditionalOnMissingBean(name = "groovyTemplateViewResolver")
public GroovyTemplateViewResolver groovyTemplateViewResolver(TemplateEngine engine) {
GroovyTemplateViewResolver resolver = new GroovyTemplateViewResolver();
resolver.setPrefix(this.properties.getPrefix());
resolver.setSuffix(this.properties.getSuffix());
resolver.setCache(this.properties.isCache());
resolver.setContentType(this.properties.getContentType());
resolver.setCharSet(this.properties.getCharSet());
resolver.setViewNames(this.properties.getViewNames());
resolver.setTemplateEngine(engine);
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 6);
return resolver;
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ClassUtils;
/**
* {@link TemplateAvailabilityProvider} that provides availability information for Groovy
* view templates
*
* @author Dave Syer
* @since 1.1.0
*/
public class GroovyTemplateAvailabilityProvider implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment,
ClassLoader classLoader, ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("groovy.text.TemplateEngine", classLoader)) {
String prefix = environment.getProperty("spring.groovy.template.prefix",
GroovyTemplateProperties.DEFAULT_PREFIX);
String suffix = environment.getProperty("spring.groovy.template.suffix",
GroovyTemplateProperties.DEFAULT_SUFFIX);
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template;
import groovy.text.markup.TemplateConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Dave Syer
* @since 1.1.0
*/
@ConfigurationProperties(prefix = "spring.groovy.template")
public class GroovyTemplateProperties {
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".tpl";
private String prefix = DEFAULT_PREFIX;
private String suffix = DEFAULT_SUFFIX;
private boolean cache;
private String contentType = "text/html";
private String charSet = "UTF-8";
private String[] viewNames;
private boolean checkTemplateLocation = true;
private TemplateConfiguration configuration = new TemplateConfiguration();
public void setCheckTemplateLocation(boolean checkTemplateLocation) {
this.checkTemplateLocation = checkTemplateLocation;
}
public boolean isCheckTemplateLocation() {
return this.checkTemplateLocation;
}
public String[] getViewNames() {
return this.viewNames;
}
public void setViewNames(String[] viewNames) {
this.viewNames = viewNames;
}
public boolean isCache() {
return this.cache;
}
public void setCache(boolean cache) {
this.cache = cache;
}
public String getContentType() {
return this.contentType
+ (this.contentType.contains(";charset=") ? "" : ";charset="
+ this.charSet);
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getCharSet() {
return this.charSet;
}
public void setCharSet(String charSet) {
this.charSet = charSet;
}
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return this.suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public void setConfiguration(TemplateConfiguration configuration) {
this.configuration = configuration;
}
public TemplateConfiguration getConfiguration() {
return this.configuration;
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template.web;
import groovy.text.Template;
import java.io.BufferedWriter;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
/**
* @author Dave Syer
*
* @since 1.1.0
*/
public class GroovyTemplateView extends AbstractUrlBasedView {
private final Template template;
public GroovyTemplateView(Template template) {
this.template = template;
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
applyContentType(response);
this.template.make(model).writeTo(new BufferedWriter(response.getWriter()));
}
/**
* Apply this view's content type as specified in the "contentType" bean property to
* the given response.
* @param response current HTTP response
* @see #setContentType
*/
protected void applyContentType(HttpServletResponse response) {
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template.web;
import groovy.text.SimpleTemplateEngine;
import groovy.text.Template;
import groovy.text.TemplateEngine;
import java.io.InputStreamReader;
import java.util.Locale;
import org.springframework.beans.propertyeditors.LocaleEditor;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
/**
* @author Dave Syer
* @since 1.1.0
*/
public class GroovyTemplateViewResolver extends UrlBasedViewResolver {
private TemplateEngine engine = new SimpleTemplateEngine();
private String charSet = "UTF-8";
public GroovyTemplateViewResolver() {
setViewClass(GroovyTemplateView.class);
}
/**
* @param engine the engine to set
*/
public void setTemplateEngine(TemplateEngine engine) {
this.engine = engine;
}
/**
* @param charSet the charSet to set
*/
public void setCharSet(String charSet) {
this.charSet = charSet;
}
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
Resource resource = resolveResource(viewName, locale);
if (resource == null) {
return null;
}
Template template = this.engine.createTemplate(new InputStreamReader(resource
.getInputStream(), this.charSet));
GroovyTemplateView view = new GroovyTemplateView(template);
view.setApplicationContext(getApplicationContext());
view.setServletContext(getServletContext());
view.setContentType(getContentType());
return view;
}
private Resource resolveResource(String viewName, Locale locale) {
String l10n = "";
if (locale != null) {
LocaleEditor localeEditor = new LocaleEditor();
localeEditor.setValue(locale);
l10n = "_" + localeEditor.getAsText();
}
return resolveFromLocale(viewName, l10n);
}
private Resource resolveFromLocale(String viewName, String locale) {
Resource resource = getApplicationContext().getResource(
getPrefix() + viewName + locale + getSuffix());
if (resource == null || !resource.exists()) {
if (locale.isEmpty()) {
return null;
}
int index = locale.lastIndexOf("_");
return resolveFromLocale(viewName, locale.substring(0, index));
}
return resource;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2012-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template.web;
import groovy.text.markup.BaseTemplate;
import groovy.text.markup.MarkupTemplateEngine;
import groovy.text.markup.TemplateConfiguration;
import java.util.Map;
import org.springframework.context.i18n.LocaleContextHolder;
/**
* @author Dave Syer
*/
public abstract class LocaleAwareTemplate extends BaseTemplate {
public LocaleAwareTemplate(MarkupTemplateEngine templateEngine, Map<?, ?> model,
Map<String, String> modelTypes, TemplateConfiguration configuration) {
super(localize(templateEngine), model, modelTypes, localize(configuration));
}
private static MarkupTemplateEngine localize(MarkupTemplateEngine templateEngine) {
TemplateConfiguration templateConfiguration = templateEngine
.getTemplateConfiguration();
ClassLoader parent = templateEngine.getTemplateLoader().getParent();
return new MarkupTemplateEngine(parent, localize(templateConfiguration));
}
private static TemplateConfiguration localize(TemplateConfiguration configuration) {
TemplateConfiguration result = new TemplateConfiguration(configuration);
result.setLocale(LocaleContextHolder.getLocale());
return result;
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.groovy.template;
import groovy.text.TemplateEngine;
import java.io.File;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.groovy.template.web.GroovyTemplateViewResolver;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.RequestContext;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
/**
* Tests for GroovyTemplateAutoConfiguration.
*
* @author Dave Syer
*/
public class GroovyTemplateAutoConfigurationTests {
private AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
@Before
public void setupContext() {
this.context.setServletContext(new MockServletContext());
}
@After
public void close() {
LocaleContextHolder.resetLocaleContext();
if (this.context != null) {
this.context.close();
}
}
@Test
public void defaultConfiguration() {
registerAndRefreshContext();
assertThat(this.context.getBean(GroovyTemplateViewResolver.class), notNullValue());
}
@Test(expected = BeanCreationException.class)
public void nonExistentTemplateLocation() {
registerAndRefreshContext("spring.groovy.template.prefix:"
+ "classpath:/does-not-exist/");
}
@Test
public void emptyTemplateLocation() {
new File("target/test-classes/templates/empty-directory").mkdir();
registerAndRefreshContext("spring.groovy.template.prefix:"
+ "classpath:/templates/empty-directory/");
}
@Test
public void defaultViewResolution() throws Exception {
registerAndRefreshContext();
MockHttpServletResponse response = render("home");
String result = response.getContentAsString();
assertThat(result, containsString("home"));
assertThat(response.getContentType(), equalTo("text/html;charset=UTF-8"));
}
@Test
public void includesViewResolution() throws Exception {
registerAndRefreshContext();
MockHttpServletResponse response = render("includes");
String result = response.getContentAsString();
assertThat(result, containsString("here"));
assertThat(response.getContentType(), equalTo("text/html;charset=UTF-8"));
}
@Test
public void localeViewResolution() throws Exception {
LocaleContextHolder.setLocale(Locale.FRENCH);
registerAndRefreshContext();
MockHttpServletResponse response = render("includes", Locale.FRENCH);
String result = response.getContentAsString();
assertThat(result, containsString("voila"));
assertThat(response.getContentType(), equalTo("text/html;charset=UTF-8"));
}
@Test
public void customContentType() throws Exception {
registerAndRefreshContext("spring.groovy.template.contentType:application/json");
MockHttpServletResponse response = render("home");
String result = response.getContentAsString();
assertThat(result, containsString("home"));
assertThat(response.getContentType(), equalTo("application/json;charset=UTF-8"));
}
@Test
public void customPrefix() throws Exception {
registerAndRefreshContext("spring.groovy.template.prefix:classpath:/templates/prefix/");
MockHttpServletResponse response = render("prefixed");
String result = response.getContentAsString();
assertThat(result, containsString("prefixed"));
}
@Test
public void customSuffix() throws Exception {
registerAndRefreshContext("spring.groovy.template.suffix:.groovytemplate");
MockHttpServletResponse response = render("suffixed");
String result = response.getContentAsString();
assertThat(result, containsString("suffixed"));
}
@Test
public void customTemplateLoaderPath() throws Exception {
registerAndRefreshContext("spring.groovy.template.prefix:classpath:/custom-templates/");
MockHttpServletResponse response = render("custom");
String result = response.getContentAsString();
assertThat(result, containsString("custom"));
}
@Test
public void disableCache() {
registerAndRefreshContext("spring.groovy.template.cache:false");
assertThat(
this.context.getBean(GroovyTemplateViewResolver.class).getCacheLimit(),
equalTo(0));
}
@Test
public void renderTemplate() throws Exception {
registerAndRefreshContext();
TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Writer writer = new StringWriter();
engine.createTemplate(new ClassPathResource("templates/message.tpl").getFile())
.make(new HashMap<String, Object>(Collections.singletonMap("greeting",
"Hello World"))).writeTo(writer);
assertThat(writer.toString(), containsString("Hello World"));
}
private void registerAndRefreshContext(String... env) {
EnvironmentTestUtils.addEnvironment(this.context, env);
this.context.register(GroovyTemplateAutoConfiguration.class);
this.context.refresh();
}
private MockHttpServletResponse render(String viewName) throws Exception {
return render(viewName, Locale.UK);
}
private MockHttpServletResponse render(String viewName, Locale locale)
throws Exception {
GroovyTemplateViewResolver resolver = this.context
.getBean(GroovyTemplateViewResolver.class);
View view = resolver.resolveViewName(viewName, locale);
assertThat(view, notNullValue());
HttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(RequestContext.WEB_APPLICATION_CONTEXT_ATTRIBUTE,
this.context);
MockHttpServletResponse response = new MockHttpServletResponse();
view.render(null, request, response);
return response;
}
}

View File

@ -0,0 +1 @@
yield "custom"

View File

@ -0,0 +1 @@
yield "home"

View File

@ -0,0 +1 @@
yield "here"

View File

@ -0,0 +1 @@
yield "voila"

View File

@ -0,0 +1,2 @@
yield "include"
include template: "included.tpl"

View File

@ -0,0 +1 @@
yield "Message: ${greeting}"

View File

@ -0,0 +1 @@
yield "prefixed"

View File

@ -0,0 +1,3 @@
yield """
suffixed
"""

View File

@ -106,8 +106,9 @@
<spring-plugin.version>1.1.0.RELEASE</spring-plugin.version>
<spring-amqp.version>1.3.2.RELEASE</spring-amqp.version>
<spring-mobile.version>1.1.1.RELEASE</spring-mobile.version>
<spring-retry.version>1.1.0.RELEASE</spring-retry.version>
<spring-security.version>3.2.3.RELEASE</spring-security.version>
<spring-security-oauth.version>2.0.0.RC2</spring-security-oauth.version>
<spring-security-oauth.version>2.0.0.RELEASE</spring-security-oauth.version>
<spring-security-jwt.version>1.0.2.RELEASE</spring-security-jwt.version>
<thymeleaf.version>2.1.3.RELEASE</thymeleaf.version>
<thymeleaf-extras-springsecurity3.version>2.1.1.RELEASE</thymeleaf-extras-springsecurity3.version>
@ -994,6 +995,11 @@
<artifactId>spring-mobile-device</artifactId>
<version>${spring-mobile.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>${spring-retry.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>