Add script based templating support

This commit adds support for script based templating. Any templating
library running on top of a JSR-223 ScriptEngine that implements
Invocable like Nashorn or JRuby could be used.

For example, in order to render Mustache templates thanks to the Nashorn
Javascript engine provided with Java 8+, you should declare the following
configuration:

@Configuration
@EnableWebMvc
public class MustacheConfig extends WebMvcConfigurerAdapter {

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.scriptTemplate();
	}

	@Bean
	public ScriptTemplateConfigurer configurer() {
		ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
		configurer.setEngineName("nashorn");
		configurer.setScripts("mustache.js");
		configurer.setRenderObject("Mustache");
		configurer.setRenderFunction("render");
		return configurer;
	}
}

The XML counterpart is:

<beans>
	<mvc:annotation-driven />

	<mvc:view-resolvers>
		<mvc:script-template />
	</mvc:view-resolvers>

	<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
		<mvc:script location="mustache.js" />
	</mvc:script-template-configurer>
</beans>

Tested with:
 - Handlebars running on Nashorn
 - Mustache running on Nashorn
 - React running on Nashorn
 - EJS running on Nashorn
 - ERB running on JRuby
 - String templates running on Jython

Issue: SPR-12266
This commit is contained in:
Sebastien Deleuze 2015-03-17 18:14:05 +01:00
parent b6327acec8
commit a3159dfbf2
36 changed files with 1792 additions and 16 deletions

View File

@ -47,6 +47,7 @@ configure(allprojects) { project ->
ext.javamailVersion = "1.5.3"
ext.jettyVersion = "9.2.10.v20150310"
ext.jodaVersion = "2.7"
ext.jrubyVersion = "1.7.19"
ext.jtaVersion = "1.2"
ext.junitVersion = "4.12"
ext.nettyVersion = "4.0.26.Final"
@ -469,7 +470,7 @@ project("spring-context") {
optional("org.aspectj:aspectjweaver:${aspectjVersion}")
optional("org.codehaus.groovy:groovy-all:${groovyVersion}")
optional("org.beanshell:bsh:2.0b4")
optional("org.jruby:jruby:1.7.19")
optional("org.jruby:jruby:${jrubyVersion}")
testCompile("javax.inject:javax.inject-tck:1")
testCompile("org.javamoney:moneta:1.0-RC3")
testCompile("commons-dbcp:commons-dbcp:1.4")
@ -898,6 +899,12 @@ project("spring-webmvc") {
testCompile("commons-io:commons-io:1.3")
testCompile("joda-time:joda-time:${jodaVersion}")
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
testCompile("org.webjars:mustachejs:0.8.2")
testCompile("org.webjars:handlebars:3.0.0-1")
testCompile("org.webjars:react:0.12.2")
testCompile("org.webjars:underscorejs:1.8.2")
testCompile("org.jruby:jruby:${jrubyVersion}")
testCompile("org.python:jython-standalone:2.5.3")
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -24,6 +24,7 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
*
* @author Keith Donald
* @author Jeremy Grelle
* @author Sebastien Deleuze
* @since 3.0
*/
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@ -42,6 +43,7 @@ public class MvcNamespaceHandler extends NamespaceHandlerSupport {
registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser());
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2002-2015 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.web.servlet.config;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;
/**
* Parse the <mvc:script-template-configurer> MVC namespace element and register a
* {@code ScriptTemplateConfigurer} bean.
*
* @author Sebastien Deleuze
* @since 4.2
*/
public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser {
public static final String BEAN_NAME = "mvcScriptTemplateConfigurer";
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) {
return BEAN_NAME;
}
@Override
protected String getBeanClassName(Element element) {
return "org.springframework.web.servlet.view.script.ScriptTemplateConfigurer";
}
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
List<Element> childElements = DomUtils.getChildElementsByTagName(element, "script");
if (!childElements.isEmpty()) {
List<String> locations = new ArrayList<String>(childElements.size());
for (Element childElement : childElements) {
locations.add(childElement.getAttribute("location"));
}
builder.addPropertyValue("scripts", locations.toArray(new String[locations.size()]));
}
builder.addPropertyValue("engineName", element.getAttribute("engine-name"));
if (element.hasAttribute("render-object")) {
builder.addPropertyValue("renderObject", element.getAttribute("render-object"));
}
if (element.hasAttribute("render-function")) {
builder.addPropertyValue("renderFunction", element.getAttribute("render-function"));
}
if (element.hasAttribute("charset")) {
builder.addPropertyValue("charset", Charset.forName(element.getAttribute("charset")));
}
if (element.hasAttribute("resource-loader-path")) {
builder.addPropertyValue("resourceLoaderPath", element.getAttribute("resource-loader-path"));
}
}
@Override
protected boolean isEligibleAttribute(String name) {
return (name.equals("engine-name") || name.equals("scripts") || name.equals("render-object") ||
name.equals("render-function") || name.equals("charset") || name.equals("resource-loader-path"));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -37,6 +37,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.ViewResolverComposite;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityViewResolver;
@ -60,6 +61,8 @@ import org.springframework.web.servlet.view.velocity.VelocityViewResolver;
* @see TilesConfigurerBeanDefinitionParser
* @see FreeMarkerConfigurerBeanDefinitionParser
* @see VelocityConfigurerBeanDefinitionParser
* @see GroovyMarkupConfigurerBeanDefinitionParser
* @see ScriptTemplateConfigurerBeanDefinitionParser
*/
public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
@ -72,7 +75,7 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
ManagedList<Object> resolvers = new ManagedList<Object>(4);
resolvers.setSource(context.extractSource(element));
String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "bean", "ref"};
String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "script-template", "bean", "ref"};
for (Element resolverElement : DomUtils.getChildElementsByTagName(element, names)) {
String name = resolverElement.getLocalName();
@ -106,6 +109,10 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
resolverBeanDef.getPropertyValues().add("suffix", ".tpl");
addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef);
}
else if ("script-template".equals(name)) {
resolverBeanDef = new RootBeanDefinition(ScriptTemplateViewResolver.class);
addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef);
}
else if ("bean-name".equals(name)) {
resolverBeanDef = new RootBeanDefinition(BeanNameViewResolver.class);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -37,6 +37,8 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;
@ -233,6 +235,22 @@ public class ViewResolverRegistry {
return registration;
}
/**
* Register a script template view resolver with an empty default view name prefix and suffix.
* @since 4.2
*/
public UrlBasedViewResolverRegistration scriptTemplate() {
if (this.applicationContext != null && !hasBeanOfType(ScriptTemplateConfigurer.class)) {
throw new BeanInitializationException("In addition to a script template view resolver " +
"there must also be a single ScriptTemplateConfig bean in this web application context " +
"(or its parent): ScriptTemplateConfigurer is the usual implementation. " +
"This bean may be given any name.");
}
ScriptRegistration registration = new ScriptRegistration();
this.viewResolvers.add(registration.getViewResolver());
return registration;
}
/**
* Register a bean name view resolver that interprets view names as the names
* of {@link org.springframework.web.servlet.View} beans.
@ -324,4 +342,12 @@ public class ViewResolverRegistry {
}
}
private static class ScriptRegistration extends UrlBasedViewResolverRegistration {
private ScriptRegistration() {
super(new ScriptTemplateViewResolver());
getViewResolver();
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.nio.charset.Charset;
import javax.script.ScriptEngine;
import org.springframework.core.io.ResourceLoader;
/**
* Interface to be implemented by objects that configure and manage a
* {@link ScriptEngine} for automatic lookup in a web environment.
* Detected and used by {@link ScriptTemplateView}.
*
* @author Sebastien Deleuze
* @since 4.2
*/
public interface ScriptTemplateConfig {
ScriptEngine getEngine();
String getRenderObject();
String getRenderFunction();
Charset getCharset();
ResourceLoader getResourceLoader();
}

View File

@ -0,0 +1,257 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* An implementation of Spring MVC's {@link ScriptTemplateConfig} for creating
* a {@code ScriptEngine} for use in a web application.
*
* <pre class="code">
*
* // Add the following to an &#64;Configuration class
*
* &#64;Bean
* public ScriptTemplateConfigurer mustacheConfigurer() {
*
* ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
* configurer.setEngineName("nashorn");
* configurer.setScripts("mustache.js");
* configurer.setRenderObject("Mustache");
* configurer.setRenderFunction("render");
* return configurer;
* }
* </pre>
*
* @author Sebastien Deleuze
* @since 4.2
* @see ScriptTemplateView
*/
public class ScriptTemplateConfigurer implements ScriptTemplateConfig, ApplicationContextAware, InitializingBean {
private ScriptEngine engine;
private String engineName;
private ApplicationContext applicationContext;
private String[] scripts;
private String renderObject;
private String renderFunction;
private Charset charset = Charset.forName("UTF-8");
private ResourceLoader resourceLoader;
private String resourceLoaderPath = "classpath:";
/**
* Set the {@link ScriptEngine} to use by the view.
* The script engine must implement {@code Invocable}.
* You must define {@code engine} or {@code engineName}, not both.
*/
public void setEngine(ScriptEngine engine) {
Assert.isInstanceOf(Invocable.class, engine);
this.engine = engine;
}
@Override
public ScriptEngine getEngine() {
return this.engine;
}
/**
* Set the engine name that will be used to instantiate the {@link ScriptEngine}.
* The script engine must implement {@code Invocable}.
* You must define {@code engine} or {@code engineName}, not both.
*/
public void setEngineName(String engineName) {
this.engineName = engineName;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
protected ApplicationContext getApplicationContext() {
return this.applicationContext;
}
/**
* Set the scripts to be loaded by the script engine (library or user provided).
* Since {@code resourceLoaderPath} default value is "classpath:", you can load easily
* any script available on the classpath.
*
* For example, in order to use a Javascript library available as a WebJars dependency
* and a custom "render.js" file, you should call
* {@code configurer.setScripts("/META-INF/resources/webjars/library/version/library.js",
* "com/myproject/script/render.js");}.
*
* @see #setResourceLoaderPath(String)
* @see <a href="http://www.webjars.org">WebJars</a>
*/
public void setScripts(String... scriptNames) {
this.scripts = scriptNames;
}
@Override
public String getRenderObject() {
return renderObject;
}
/**
* Set the object where belongs the render function (optional).
* For example, in order to call {@code Mustache.render()}, {@code renderObject}
* should be set to {@code "Mustache"} and {@code renderFunction} to {@code "render"}.
*/
public void setRenderObject(String renderObject) {
this.renderObject = renderObject;
}
@Override
public String getRenderFunction() {
return renderFunction;
}
/**
* Set the render function name (mandatory). This function will be called with the
* following parameters:
* <ol>
* <li>{@code template}: the view template content (String)</li>
* <li>{@code model}: the view model (Map)</li>
* </ol>
*/
public void setRenderFunction(String renderFunction) {
this.renderFunction = renderFunction;
}
/**
* Set the charset used to read script and template files.
* ({@code UTF-8} by default).
*/
public void setCharset(Charset charset) {
this.charset = charset;
}
@Override
public Charset getCharset() {
return this.charset;
}
/**
* Set the resource loader path(s) via a Spring resource location.
* Accepts multiple locations as a comma-separated list of paths.
* Standard URLs like "file:" and "classpath:" and pseudo URLs are supported
* as understood by Spring's {@link org.springframework.core.io.ResourceLoader}.
* Relative paths are allowed when running in an ApplicationContext.
* Default is "classpath:".
*/
public void setResourceLoaderPath(String resourceLoaderPath) {
this.resourceLoaderPath = resourceLoaderPath;
}
public String getResourceLoaderPath() {
return resourceLoaderPath;
}
@Override
public ResourceLoader getResourceLoader() {
return resourceLoader;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.engine == null) {
this.engine = createScriptEngine();
}
Assert.state(this.renderFunction != null, "renderFunction property must be defined.");
this.resourceLoader = new DefaultResourceLoader(createClassLoader());
if (this.scripts != null) {
try {
for (String script : this.scripts) {
this.engine.eval(read(script));
}
}
catch (ScriptException e) {
throw new IllegalStateException("could not load script", e);
}
}
}
protected ClassLoader createClassLoader() throws IOException {
String[] paths = StringUtils.commaDelimitedListToStringArray(this.resourceLoaderPath);
List<URL> urls = new ArrayList<URL>();
for (String path : paths) {
Resource[] resources = getApplicationContext().getResources(path);
if (resources.length > 0) {
for (Resource resource : resources) {
if (resource.exists()) {
urls.add(resource.getURL());
}
}
}
}
ClassLoader classLoader = getApplicationContext().getClassLoader();
return (urls.size() > 0 ? new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader) : classLoader);
}
private Reader read(String path) throws IOException {
Resource resource = this.resourceLoader.getResource(path);
Assert.state(resource.exists(), "Resource " + path + " not found.");
return new InputStreamReader(resource.getInputStream());
}
protected ScriptEngine createScriptEngine() throws IOException {
if (this.engine != null && this.engineName != null) {
throw new IllegalStateException("You should define engine or engineName properties, not both.");
}
if (this.engineName != null) {
ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName(this.engineName);
Assert.state(scriptEngine != null, "No engine \"" + this.engineName + "\" found.");
Assert.state(scriptEngine instanceof Invocable, "Script engine should be instance of Invocable");
this.engine = scriptEngine;
}
Assert.state(this.engine != null, "No script engine found, please specify valid engine or engineName properties.");
return this.engine;
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Map;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
/**
* An {@link org.springframework.web.servlet.view.AbstractUrlBasedView AbstractUrlBasedView}
* designed to run any template library based on a JSR-223 script engine.
*
* <p>Nashorn Javascript engine requires Java 8+.
*
* @author Sebastien Deleuze
* @since 4.2
* @see ScriptTemplateConfigurer
* @see ScriptTemplateViewResolver
*/
public class ScriptTemplateView extends AbstractUrlBasedView {
private ScriptEngine engine;
private String renderObject;
private String renderFunction;
private Charset charset;
private ResourceLoader resourceLoader;
/**
* Set the {@link ScriptEngine} to use in this view.
* <p>If not set, the engine is auto-detected by looking up up a single
* {@link ScriptTemplateConfig} bean in the web application context and using
* it to obtain the configured {@code ScriptEngine} instance.
* @see ScriptTemplateConfig
*/
public void setEngine(ScriptEngine engine) {
this.engine = engine;
}
/**
* Set the render function name. This function will be called with the
* following parameters:
* <ol>
* <li>{@code template}: the view template content (String)</li>
* <li>{@code model}: the view model (Map)</li>
* </ol>
* <p>If not set, the function name is auto-detected by looking up up a single
* {@link ScriptTemplateConfig} bean in the web application context and using
* it to obtain the configured {@code functionName} property.
* @see ScriptTemplateConfig
*/
public void setRenderFunction(String functionName) {
this.renderFunction = functionName;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
@Override
protected void initApplicationContext(ApplicationContext context) {
super.initApplicationContext(context);
ScriptTemplateConfig viewConfig = autodetectViewConfig();
if (this.engine == null) {
this.engine = viewConfig.getEngine();
Assert.state(this.engine != null, "Script engine should not be null.");
Assert.state(this.engine instanceof Invocable, "Script engine should be instance of Invocable");
}
if (this.resourceLoader == null) {
this.resourceLoader = viewConfig.getResourceLoader();
}
if (this.renderObject == null) {
this.renderObject = viewConfig.getRenderObject();
}
if (this.renderFunction == null) {
this.renderFunction = viewConfig.getRenderFunction();
}
if (this.charset == null) {
this.charset = viewConfig.getCharset();
}
}
protected ScriptTemplateConfig autodetectViewConfig() throws BeansException {
try {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(getApplicationContext(), ScriptTemplateConfig.class, true, false);
}
catch (NoSuchBeanDefinitionException ex) {
throw new ApplicationContextException("Expected a single ScriptTemplateConfig bean in the current " +
"Servlet web application context or the parent root context: ScriptTemplateConfigurer is " +
"the usual implementation. This bean may have any name.", ex);
}
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
Assert.notNull("Render function must not be null", this.renderFunction);
try {
String template = getTemplate(getUrl());
Object html = null;
if (this.renderObject != null) {
Object thiz = engine.eval(this.renderObject);
html = ((Invocable)this.engine).invokeMethod(thiz, this.renderFunction, template, model);
}
else {
html = ((Invocable)this.engine).invokeFunction(this.renderFunction, template, model);
}
response.getWriter().write(String.valueOf(html));
}
catch (Exception e) {
throw new IllegalStateException("failed to render template", e);
}
}
protected String getTemplate(String path) throws IOException {
Resource resource = this.resourceLoader.getResource(path);
Assert.state(resource.exists(), "Resource " + path + " not found.");
return StreamUtils.copyToString(resource.getInputStream(), this.charset);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
/**
* Convenience subclass of
* {@link org.springframework.web.servlet.view.UrlBasedViewResolver}
* that supports {@link ScriptTemplateView} and custom subclasses of it.
*
* <p>The view class for all views created by this resolver can be specified
* via the {@link #setViewClass(Class)} property.
*
* <p><b>Note:</b> When chaining ViewResolvers this resolver will check for the
* existence of the specified template resources and only return a non-null
* View object if a template is actually found.
*
* @author Sebastien Deleuze
* @since 4.2
* @see ScriptTemplateConfigurer
*/
public class ScriptTemplateViewResolver extends UrlBasedViewResolver {
public ScriptTemplateViewResolver() {
setViewClass(requiredViewClass());
}
@Override
protected Class<?> requiredViewClass() {
return ScriptTemplateView.class;
}
}

View File

@ -973,7 +973,7 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
Register a TilesViewResolver based on Tiles 3.x.
To configure Tiles you must also add a top-level <mvc:tiles> element
To configure Tiles you must also add a top-level <mvc:tiles-configurer> element
or declare a TilesConfigurer bean.
]]></xsd:documentation>
</xsd:annotation>
@ -983,7 +983,7 @@
<xsd:documentation><![CDATA[
Register a FreeMarkerViewResolver.
By default, ".ftl" is configured as a view name suffix.
To configure FreeMarker you must also add a top-level <mvc:freemarker> element
To configure FreeMarker you must also add a top-level <mvc:freemarker-configurer> element
or declare a FreeMarkerConfigurer bean.
]]></xsd:documentation>
</xsd:annotation>
@ -993,7 +993,7 @@
<xsd:documentation><![CDATA[
Register a VelocityViewResolver.
By default, ".vm" is configured as a view name suffix.
To configure Velocity you must also add a top-level <mvc:velocity> element
To configure Velocity you must also add a top-level <mvc:velocity-configurer> element
or declare a VelocityConfigurer bean.
]]></xsd:documentation>
</xsd:annotation>
@ -1003,11 +1003,20 @@
<xsd:documentation><![CDATA[
Register a GroovyMarkupViewResolver.
By default, ".tpl" is configured as a view name suffix.
To configure the Groovy markup template engine you must also add a top-level <mvc:groovy-markup> element
To configure the Groovy markup template engine you must also add a top-level <mvc:groovy-configurer> element
or declare a GroovyMarkupConfigurer bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="script-template" type="urlViewResolverType">
<xsd:annotation>
<xsd:documentation><![CDATA[
Register a ScriptTemplateViewResolver.
To configure the Script engine you must also add a top-level <mvc:script-template-configurer> element
or declare a ScriptTemplateConfigurer bean.
]]></xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="bean-name" maxOccurs="1">
<xsd:annotation>
<xsd:documentation><![CDATA[
@ -1163,4 +1172,66 @@
</xsd:complexType>
</xsd:element>
<xsd:element name="script-template-configurer">
<xsd:annotation>
<xsd:documentation><![CDATA[
Configure the script engine for view resolution by registering a ScriptTemplateConfigurer
bean. This is a shortcut alternative to declaring a ScriptTemplateConfigurer bean directly.
]]></xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:element name="script" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType>
<xsd:attribute name="location" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
The location of the script to be loaded by the script engine (library or user provided).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="engine-name" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
The script engine name to use by the view.
The script engine must implement Invocable.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="render-object" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The object where belong the render function.
For example, in order to call Mustache.render(), renderObject
should be set to Mustache and renderFunction to render.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="render-function" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
Set the render function name.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="charset" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Set the charset used to read script and template files (UTF-8 by default).
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="resource-loader-path" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The script engine resource loader path via a Spring resource location.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>

View File

@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -129,6 +130,8 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;
@ -720,11 +723,11 @@ public class MvcNamespaceTests {
@Test
public void testViewResolution() throws Exception {
loadBeanDefinitions("mvc-config-view-resolution.xml", 6);
loadBeanDefinitions("mvc-config-view-resolution.xml", 7);
ViewResolverComposite compositeResolver = this.appContext.getBean(ViewResolverComposite.class);
assertNotNull(compositeResolver);
assertEquals("Actual: " + compositeResolver.getViewResolvers(), 8, compositeResolver.getViewResolvers().size());
assertEquals("Actual: " + compositeResolver.getViewResolvers(), 9, compositeResolver.getViewResolvers().size());
assertEquals(Ordered.LOWEST_PRECEDENCE, compositeResolver.getOrder());
List<ViewResolver> resolvers = compositeResolver.getViewResolvers();
@ -759,8 +762,15 @@ public class MvcNamespaceTests {
assertEquals(".tpl", accessor.getPropertyValue("suffix"));
assertEquals(1024, accessor.getPropertyValue("cacheLimit"));
assertEquals(InternalResourceViewResolver.class, resolvers.get(6).getClass());
resolver = resolvers.get(6);
assertThat(resolver, instanceOf(ScriptTemplateViewResolver.class));
accessor = new DirectFieldAccessor(resolver);
assertEquals("", accessor.getPropertyValue("prefix"));
assertEquals("", accessor.getPropertyValue("suffix"));
assertEquals(1024, accessor.getPropertyValue("cacheLimit"));
assertEquals(InternalResourceViewResolver.class, resolvers.get(7).getClass());
assertEquals(InternalResourceViewResolver.class, resolvers.get(8).getClass());
TilesConfigurer tilesConfigurer = appContext.getBean(TilesConfigurer.class);
assertNotNull(tilesConfigurer);
@ -787,11 +797,21 @@ public class MvcNamespaceTests {
assertEquals("/test", groovyMarkupConfigurer.getResourceLoaderPath());
assertTrue(groovyMarkupConfigurer.isAutoIndent());
assertFalse(groovyMarkupConfigurer.isCacheTemplates());
ScriptTemplateConfigurer scriptTemplateConfigurer = appContext.getBean(ScriptTemplateConfigurer.class);
assertNotNull(scriptTemplateConfigurer);
assertEquals("Mustache", scriptTemplateConfigurer.getRenderObject());
assertEquals("render", scriptTemplateConfigurer.getRenderFunction());
assertEquals(StandardCharsets.ISO_8859_1, scriptTemplateConfigurer.getCharset());
assertEquals("classpath:", scriptTemplateConfigurer.getResourceLoaderPath());
String[] scripts = { "/META-INF/resources/webjars/mustachejs/0.8.2/mustache.js" };
accessor = new DirectFieldAccessor(scriptTemplateConfigurer);
assertArrayEquals(scripts, (String[]) accessor.getPropertyValue("scripts"));
}
@Test
public void testViewResolutionWithContentNegotiation() throws Exception {
loadBeanDefinitions("mvc-config-view-resolution-content-negotiation.xml", 6);
loadBeanDefinitions("mvc-config-view-resolution-content-negotiation.xml", 7);
ViewResolverComposite compositeResolver = this.appContext.getBean(ViewResolverComposite.class);
assertNotNull(compositeResolver);
@ -801,7 +821,7 @@ public class MvcNamespaceTests {
List<ViewResolver> resolvers = compositeResolver.getViewResolvers();
assertEquals(ContentNegotiatingViewResolver.class, resolvers.get(0).getClass());
ContentNegotiatingViewResolver cnvr = (ContentNegotiatingViewResolver) resolvers.get(0);
assertEquals(6, cnvr.getViewResolvers().size());
assertEquals(7, cnvr.getViewResolvers().size());
assertEquals(1, cnvr.getDefaultViews().size());
assertTrue(cnvr.isUseNotAcceptableStatusCode());

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -34,6 +34,8 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;
@ -60,6 +62,7 @@ public class ViewResolverRegistryTests {
context.registerSingleton("velocityConfigurer", VelocityConfigurer.class);
context.registerSingleton("tilesConfigurer", TilesConfigurer.class);
context.registerSingleton("groovyMarkupConfigurer", GroovyMarkupConfigurer.class);
context.registerSingleton("scriptTemplateConfigurer", ScriptTemplateConfigurer.class);
this.registry = new ViewResolverRegistry();
this.registry.setApplicationContext(context);
this.registry.setContentNegotiationManager(new ContentNegotiationManager());
@ -182,6 +185,20 @@ public class ViewResolverRegistryTests {
checkPropertyValues(resolver, "prefix", "", "suffix", ".tpl");
}
@Test
public void scriptTemplate() {
this.registry.scriptTemplate().prefix("/").suffix(".html").cache(true);
ScriptTemplateViewResolver resolver = checkAndGetResolver(ScriptTemplateViewResolver.class);
checkPropertyValues(resolver, "prefix", "/", "suffix", ".html", "cacheLimit", 1024);
}
@Test
public void scriptTemplateDefaultValues() {
this.registry.scriptTemplate();
ScriptTemplateViewResolver resolver = checkAndGetResolver(ScriptTemplateViewResolver.class);
checkPropertyValues(resolver, "prefix", "", "suffix", "");
}
@Test
public void contentNegotiation() {
MappingJackson2JsonView view = new MappingJackson2JsonView();

View File

@ -0,0 +1,97 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for ERB templates running on JRuby.
*
* @author Sebastien Deleuze
*/
public class ErbJrubyScriptTemplateTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/erb/template.erb", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
view.renderMergedOutputModel(model, request, response);
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer jrubyConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setScripts("org/springframework/web/servlet/view/script/erb/render.rb");
configurer.setEngineName("jruby");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for Handlebars templates running on Nashorn Javascript engine.
*
* @author Sebastien Deleuze
*/
public class HandlebarsNashornScriptTemplateTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/handlebars/template.html", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
view.renderMergedOutputModel(model, request, response);
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer handlebarsConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts( "org/springframework/web/servlet/view/script/handlebars/polyfill.js",
"/META-INF/resources/webjars/handlebars/3.0.0-1/handlebars.js",
"org/springframework/web/servlet/view/script/handlebars/render.js");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for Mustache templates running on Nashorn Javascript engine.
*
* @author Sebastien Deleuze
*/
public class MustacheNashornScriptTemplateTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/mustache/template.html", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
view.renderMergedOutputModel(model, request, response);
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer mustacheConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("/META-INF/resources/webjars/mustachejs/0.8.2/mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@ -0,0 +1,131 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for React templates running on Nashorn Javascript engine.
*
* @author Sebastien Deleuze
*/
public class ReactNashornScriptTemplateTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void renderJavascriptTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/react/template.js", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
@Test
public void renderJsxTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/react/template.jsx", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
view.renderMergedOutputModel(model, request, response);
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
if (viewUrl.endsWith(".jsx")) {
ctx.register(JsxTemplatingConfiguration.class);
}
else {
ctx.register(JavascriptTemplatingConfiguration.class);
}
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class JavascriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer reactConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts( "org/springframework/web/servlet/view/script/react/polyfill.js",
"/META-INF/resources/webjars/react/0.12.2/react.js",
"/META-INF/resources/webjars/react/0.12.2/JSXTransformer.js",
"org/springframework/web/servlet/view/script/react/render.js");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
static class JsxTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer reactConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts( "org/springframework/web/servlet/view/script/react/polyfill.js",
"/META-INF/resources/webjars/react/0.12.2/react.js",
"/META-INF/resources/webjars/react/0.12.2/JSXTransformer.js",
"org/springframework/web/servlet/view/script/react/render.js");
configurer.setRenderFunction("renderJsx");
return configurer;
}
}
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import org.hamcrest.Matchers;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import org.springframework.context.support.StaticApplicationContext;
/**
* Unit tests for {@link ScriptTemplateConfigurer}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateConfigurerTests {
private static final String RESOURCE_LOADER_PATH = "classpath:org/springframework/web/servlet/view/script/";
private StaticApplicationContext applicationContext;
private ScriptTemplateConfigurer configurer;
@Before
public void setup() throws Exception {
this.applicationContext = new StaticApplicationContext();
this.configurer = new ScriptTemplateConfigurer();
this.configurer.setResourceLoaderPath(RESOURCE_LOADER_PATH);
}
@Test
public void customEngineAndRenderFunction() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(InvocableScriptEngine.class);
given(engine.get("key")).willReturn("value");
this.configurer.setEngine(engine);
this.configurer.setRenderFunction("render");
this.configurer.afterPropertiesSet();
engine = this.configurer.getEngine();
assertNotNull(engine);
assertEquals("value", engine.get("key"));
assertNull(this.configurer.getRenderObject());
assertEquals("render", this.configurer.getRenderFunction());
assertEquals(StandardCharsets.UTF_8, this.configurer.getCharset());
}
@Test(expected = IllegalArgumentException.class)
public void nonInvocableScriptEngine() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(ScriptEngine.class);
this.configurer.setEngine(engine);
}
@Test(expected = IllegalStateException.class)
public void noRenderFunctionDefined() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ScriptEngine engine = mock(InvocableScriptEngine.class);
this.configurer.setEngine(engine);
this.configurer.afterPropertiesSet();
}
@Test
public void parentLoader() throws Exception {
this.configurer.setApplicationContext(this.applicationContext);
ClassLoader classLoader = this.configurer.createClassLoader();
assertNotNull(classLoader);
URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(1));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/script/"));
this.configurer.setResourceLoaderPath(RESOURCE_LOADER_PATH + ",classpath:org/springframework/web/servlet/view/");
classLoader = this.configurer.createClassLoader();
assertNotNull(classLoader);
urlClassLoader = (URLClassLoader) classLoader;
assertThat(Arrays.asList(urlClassLoader.getURLs()), Matchers.hasSize(2));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(0).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/script/"));
assertThat(Arrays.asList(urlClassLoader.getURLs()).get(1).toString(),
Matchers.endsWith("org/springframework/web/servlet/view/"));
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2002-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.web.servlet.view.script;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.DirectFieldAccessor;
/**
* Unit tests for {@link ScriptTemplateViewResolver}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewResolverTests {
@Test
public void viewClass() throws Exception {
ScriptTemplateViewResolver resolver = new ScriptTemplateViewResolver();
Assert.assertEquals(ScriptTemplateView.class, resolver.requiredViewClass());
DirectFieldAccessor viewAccessor = new DirectFieldAccessor(resolver);
Class viewClass = (Class) viewAccessor.getPropertyValue("viewClass");
Assert.assertEquals(ScriptTemplateView.class, viewClass);
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.context.ApplicationContextException;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for {@link ScriptTemplateView}.
*
* @author Sebastien Deleuze
*/
public class ScriptTemplateViewTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void missingScriptTemplateConfig() throws Exception {
ScriptTemplateView view = new ScriptTemplateView();
given(this.webAppContext.getBeansOfType(ScriptTemplateConfig.class, true, false))
.willReturn(new HashMap<String, ScriptTemplateConfig>());
view.setUrl("sampleView");
try {
view.setApplicationContext(this.webAppContext);
fail();
}
catch (ApplicationContextException ex) {
assertTrue(ex.getMessage().contains("ScriptTemplateConfig"));
}
}
@Test
public void dectectScriptTemplateConfig() throws Exception {
InvocableScriptEngine engine = mock(InvocableScriptEngine.class);
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngine(engine);
configurer.setRenderObject("Template");
configurer.setRenderFunction("render");
configurer.setCharset(StandardCharsets.ISO_8859_1);
Map<String, ScriptTemplateConfig> configMap = new HashMap<String, ScriptTemplateConfig>();
configMap.put("scriptTemplateConfigurer", configurer);
ScriptTemplateView view = new ScriptTemplateView();
given(this.webAppContext.getBeansOfType(ScriptTemplateConfig.class, true, false)).willReturn(configMap);
DirectFieldAccessor accessor = new DirectFieldAccessor(view);
view.setUrl("sampleView");
view.setApplicationContext(this.webAppContext);
assertEquals(engine, accessor.getPropertyValue("engine"));
assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("charset"));
assertEquals("Template", accessor.getPropertyValue("renderObject"));
assertEquals("render", accessor.getPropertyValue("renderFunction"));
}
@Test(expected = IllegalArgumentException.class)
public void nonInvocableScriptEngine() throws Exception {
ScriptEngine engine = mock(ScriptEngine.class);
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngine(engine);
fail();
}
private interface InvocableScriptEngine extends ScriptEngine, Invocable {
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright 2002-2015 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.web.servlet.view.script;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletContext;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockServletContext;
import org.springframework.web.context.WebApplicationContext;
/**
* Unit tests for String templates running on Jython.
*
* @author Sebastien Deleuze
*/
public class StringJythonScriptTemplateTests {
private WebApplicationContext webAppContext;
private ServletContext servletContext;
@Before
public void setup() {
this.webAppContext = mock(WebApplicationContext.class);
this.servletContext = new MockServletContext();
this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.webAppContext);
}
@Test
public void renderTemplate() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("title", "Layout example");
model.put("body", "This is the body");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/python/template.html", model);
assertEquals("<html><head><title>Layout example</title></head><body><p>This is the body</p></body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest();
view.renderMergedOutputModel(model, request, response);
return response;
}
private ScriptTemplateView createViewWithUrl(String viewUrl) throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ScriptTemplatingConfiguration.class);
ctx.refresh();
ScriptTemplateView view = new ScriptTemplateView();
view.setApplicationContext(ctx);
view.setUrl(viewUrl);
view.afterPropertiesSet();
return view;
}
@Configuration
static class ScriptTemplatingConfiguration {
@Bean
public ScriptTemplateConfigurer jythonConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setScripts("org/springframework/web/servlet/view/script/python/render.py");
configurer.setEngineName("jython");
configurer.setRenderFunction("render");
return configurer;
}
}
}

View File

@ -26,6 +26,7 @@
<mvc:freemarker />
<mvc:velocity />
<mvc:groovy />
<mvc:script-template />
</mvc:view-resolvers>
<mvc:tiles-configurer check-refresh="true">
@ -40,5 +41,7 @@
<mvc:groovy-configurer />
<mvc:script-template-configurer engine-name="nashorn" render-function="render"/>
</beans>

View File

@ -13,6 +13,7 @@
<mvc:freemarker prefix="freemarker-" suffix=".freemarker" view-names="my*,*Report" />
<mvc:velocity cache-views="false" />
<mvc:groovy />
<mvc:script-template />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" />
<ref bean="customResolver" />
</mvc:view-resolvers>
@ -33,5 +34,9 @@
<mvc:groovy-configurer resource-loader-path="/test" cache-templates="false" auto-indent="true" />
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render" charset="ISO-8859-1" resource-loader-path="classpath:">
<mvc:script location="/META-INF/resources/webjars/mustachejs/0.8.2/mustache.js" />
</mvc:script-template-configurer>
</beans>

View File

@ -0,0 +1,21 @@
require 'erb'
require 'ostruct'
require 'java'
# Renders an ERB template against a hashmap of variables.
# template should be a Java InputStream
def render(template, variables)
context = OpenStruct.new(variables).instance_eval do
variables.each do |k, v|
instance_variable_set(k, v) if k[0] == '@'
end
def partial(partial_name, options={})
new_variables = marshal_dump.merge(options[:locals] || {})
Java::Pavo::ERB.render(partial_name, new_variables)
end
binding
end
ERB.new(template).result(context);
end

View File

@ -0,0 +1 @@
<html><head><title><%= title %></title></head><body><p><%= body %></p></body></html>

View File

@ -0,0 +1,5 @@
// TODO Manage compiled template cache
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}

View File

@ -0,0 +1 @@
<html><head><title>{{title}}</title></head><body><p>{{body}}</p></body></html>

View File

@ -0,0 +1 @@
<html><head><title>{{title}}</title></head><body><p>{{body}}</p></body></html>

View File

@ -0,0 +1,5 @@
from string import Template
def render(template, model):
s = Template(template)
return s.substitute(model)

View File

@ -0,0 +1 @@
<html><head><title>$title</title></head><body><p>$body</p></body></html>

View File

@ -0,0 +1,5 @@
var global = this;
var console = {};
console.debug = print;
console.warn = print;
console.log = print;

View File

@ -0,0 +1,13 @@
function render(template, model) {
// Create a real Javascript Object from the model Map
var data = {};
for(var k in model) data[k]=model[k];
var element = React.createElement(eval(template), data);
// Should use React.renderToString in production
return React.renderToStaticMarkup(element);
}
function renderJsx(template, model) {
var jsTemplate = JSXTransformer.transform(template).code;
return render(jsTemplate, model);
}

View File

@ -0,0 +1,5 @@
React.createClass({
render: function() {
return React.createElement("html", null, React.createElement("head", null, React.createElement("title", null, this.props.title)), React.createElement("body", null, React.createElement("p", null, this.props.body)));
}
});

View File

@ -0,0 +1,5 @@
React.createClass({
render: function() {
return <html><head><title>{this.props.title}</title></head><body><p>{this.props.body}</p></body></html>
}
});

View File

@ -4762,7 +4762,7 @@ And the same in XML:
</mvc:view-resolvers>
----
Note however that FreeMarker, Velocity, Tiles, and Groovy Markup also require
Note however that FreeMarker, Velocity, Tiles, Groovy Markup and script templates also require
configuration of the underlying view technology.
The MVC namespace provides dedicated elements. For example with FreeMarker:

View File

@ -2357,6 +2357,192 @@ https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-sp
[[view-script]]
== Script templates
It is possible to integrate any templating library running on top of a JSR-223
script engine in web applications using Spring. The following describes in a
broad way how to do this. The script engine must implement both `ScriptEngine`
and `Invocable` interfaces.
It has been tested with:
* http://handlebarsjs.com/[Handlebars] running on http://openjdk.java.net/projects/nashorn/[Nashorn]
* https://mustache.github.io/[Mustache] running on http://openjdk.java.net/projects/nashorn/[Nashorn]
* http://facebook.github.io/react/[React] running on http://openjdk.java.net/projects/nashorn/[Nashorn]
* http://www.embeddedjs.com/[EJS] running on http://openjdk.java.net/projects/nashorn/[Nashorn]
* http://www.stuartellis.eu/articles/erb/[ERB] running on http://jruby.org[JRuby]
* https://docs.python.org/2/library/string.html#template-strings[String templates] running on http://www.jython.org/[Jython]
[[view-script-dependencies]]
=== Dependencies
To be able to use script templates integration, you need to have available in your classpath
the script engine:
* http://openjdk.java.net/projects/nashorn/[Nashorn] Javascript engine is provided builtin with Java 8+
* http://docs.oracle.com/javase/7/docs/technotes/guides/scripting/programmer_guide/#jsengine[Rhino]
Javascript engine is provided builtin with Java 6 and Java 7.
Please notice that using Rhino is not recommended since it does not
support running most template engines.
* http://jruby.org[JRuby] dependency should be added in order to get Ruby support.
* http://www.jython.org[Jython] dependency should be added in order to get Python support.
You should also need to add dependencies for your script based template engine. For example,
for Javascript you can use http://www.webjars.org/[WebJars] to add Maven/Gradle dependencies
in order to make your javascript libraries available in the classpath.
[[view-script-integrate]]
=== How to integrate script based templating
To be able to use script templates, you have to configure it in order to specify various parameters
like the script engine to use, the script files to load and what function should be called to
render the templates. This is done thanks to a `ScriptTemplateConfigurer` bean and optional script
files.
For example, in order to render Mustache templates thanks to the Nashorn Javascript engine
provided with Java 8+, you should declare the following configuration:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class MustacheConfig extends WebMvcConfigurerAdapter {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
----
The XML counterpart using MVC namespace is:
[source,xml,indent=0]
[subs="verbatim,quotes"]
----
<mvc:annotation-driven />
<mvc:view-resolvers>
<mvc:script-template />
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js" />
</mvc:script-template-configurer>
----
The controller is exactly what you should expect:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
@Controller
public class SampleController {
@RequestMapping
public ModelAndView test() {
ModelAndView mav = new ModelAndView();
mav.addObject("title", "Sample title").addObject("body", "Sample body");
mav.setViewName("template.html");
return mav;
}
}
----
And the Mustache template is:
[source,html,indent=0]
[subs="verbatim,quotes"]
----
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
----
The render function is called with the following parameters:
* template: the view template content (String)
* model: the view model (Map)
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
If your templating technology requires some customization, you may provide a script that
implements a custom render function. For example, http://handlebarsjs.com[Handlerbars]
needs to compile templates before using them, and requires a
http://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
browser facilities not available in the server-side script engine.
[source,java,indent=0]
[subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class MustacheConfig extends WebMvcConfigurerAdapter {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
return configurer;
}
}
----
`polyfill.js` only defines the `window` object needed by Handlebars to run properly:
[source,javascript,indent=0]
[subs="verbatim,quotes"]
----
var window = {};
----
This basic `render.js` implementation compiles the template before using it. A production
ready implementation should also store and reused cached templates / pre-compiled templates.
This can be done on the script side, as well as any customization you need (managing
template engine configuration for example).
[source,javascript,indent=0]
[subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
----
Check out Spring script templates unit tests
(https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[java],
https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources])
for more configuration examples.
[[view-xml-marshalling]]
== XML Marshalling View
The `MarshallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm`