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:
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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 @Configuration class
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user