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

@@ -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>
}
});