From a3159dfbf24168b95f9d3435bea15721cde8fb38 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 17 Mar 2015 18:14:05 +0100 Subject: [PATCH] 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: 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 --- build.gradle | 9 +- .../servlet/config/MvcNamespaceHandler.java | 4 +- ...emplateConfigurerBeanDefinitionParser.java | 84 ++++++ .../ViewResolversBeanDefinitionParser.java | 11 +- .../annotation/ViewResolverRegistry.java | 28 +- .../view/script/ScriptTemplateConfig.java | 44 +++ .../view/script/ScriptTemplateConfigurer.java | 257 ++++++++++++++++++ .../view/script/ScriptTemplateView.java | 152 +++++++++++ .../script/ScriptTemplateViewResolver.java | 48 ++++ .../web/servlet/config/spring-mvc-4.2.xsd | 79 +++++- .../web/servlet/config/MvcNamespaceTests.java | 30 +- .../annotation/ViewResolverRegistryTests.java | 19 +- .../script/ErbJrubyScriptTemplateTests.java | 97 +++++++ .../HandlebarsNashornScriptTemplateTests.java | 99 +++++++ .../MustacheNashornScriptTemplateTests.java | 98 +++++++ .../ReactNashornScriptTemplateTests.java | 131 +++++++++ .../script/ScriptTemplateConfigurerTests.java | 115 ++++++++ .../ScriptTemplateViewResolverTests.java | 39 +++ .../view/script/ScriptTemplateViewTests.java | 107 ++++++++ .../StringJythonScriptTemplateTests.java | 97 +++++++ ...ig-view-resolution-content-negotiation.xml | 3 + .../config/mvc-config-view-resolution.xml | 5 + .../web/servlet/view/script/erb/render.rb | 21 ++ .../web/servlet/view/script/erb/template.erb | 1 + .../view/script/handlebars/polyfill.js | 1 + .../servlet/view/script/handlebars/render.js | 5 + .../view/script/handlebars/template.html | 1 + .../view/script/mustache/template.html | 1 + .../web/servlet/view/script/python/render.py | 5 + .../servlet/view/script/python/template.html | 1 + .../web/servlet/view/script/react/polyfill.js | 5 + .../web/servlet/view/script/react/render.js | 13 + .../web/servlet/view/script/react/template.js | 5 + .../servlet/view/script/react/template.jsx | 5 + src/asciidoc/web-mvc.adoc | 2 +- src/asciidoc/web-view.adoc | 186 +++++++++++++ 36 files changed, 1792 insertions(+), 16 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ErbJrubyScriptTemplateTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/HandlebarsNashornScriptTemplateTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/MustacheNashornScriptTemplateTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ReactNashornScriptTemplateTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolverTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/StringJythonScriptTemplateTests.java create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/render.rb create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/template.erb create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/polyfill.js create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/render.js create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/template.html create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/mustache/template.html create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/render.py create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/template.html create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/polyfill.js create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/render.js create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.js create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.jsx diff --git a/build.gradle b/build.gradle index aae84a11ad..c969e83394 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,7 @@ configure(allprojects) { project -> ext.javamailVersion = "1.5.3" ext.jettyVersion = "9.2.10.v20150310" ext.jodaVersion = "2.7" + ext.jrubyVersion = "1.7.19" ext.jtaVersion = "1.2" ext.junitVersion = "4.12" ext.nettyVersion = "4.0.26.Final" @@ -469,7 +470,7 @@ project("spring-context") { optional("org.aspectj:aspectjweaver:${aspectjVersion}") optional("org.codehaus.groovy:groovy-all:${groovyVersion}") optional("org.beanshell:bsh:2.0b4") - optional("org.jruby:jruby:1.7.19") + optional("org.jruby:jruby:${jrubyVersion}") testCompile("javax.inject:javax.inject-tck:1") testCompile("org.javamoney:moneta:1.0-RC3") testCompile("commons-dbcp:commons-dbcp:1.4") @@ -898,6 +899,12 @@ project("spring-webmvc") { testCompile("commons-io:commons-io:1.3") testCompile("joda-time:joda-time:${jodaVersion}") testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}") + testCompile("org.webjars:mustachejs:0.8.2") + testCompile("org.webjars:handlebars:3.0.0-1") + testCompile("org.webjars:react:0.12.2") + testCompile("org.webjars:underscorejs:1.8.2") + testCompile("org.jruby:jruby:${jrubyVersion}") + testCompile("org.python:jython-standalone:2.5.3") } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java index 03edc2672f..c4305f0288 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java @@ -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()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java new file mode 100644 index 0000000000..eb640e2aa0 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java @@ -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 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 childElements = DomUtils.getChildElementsByTagName(element, "script"); + if (!childElements.isEmpty()) { + List locations = new ArrayList(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")); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java index 37ce259f92..ff743ba614 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java @@ -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 resolvers = new ManagedList(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); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java index 6af073c56d..c4fedda88a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java @@ -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(); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java new file mode 100644 index 0000000000..4919492b15 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java @@ -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(); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java new file mode 100644 index 0000000000..5e3c1b755a --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java @@ -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. + * + *
+ *
+ * // 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;
+ * }
+ * 
+ * + * @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 WebJars + */ + 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: + *
    + *
  1. {@code template}: the view template content (String)
  2. + *
  3. {@code model}: the view model (Map)
  4. + *
+ */ + 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 urls = new ArrayList(); + 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; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java new file mode 100644 index 0000000000..1d306e9f2e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java @@ -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. + * + *

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. + *

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: + *

    + *
  1. {@code template}: the view template content (String)
  2. + *
  3. {@code model}: the view model (Map)
  4. + *
+ *

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 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); + } + +} \ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java new file mode 100644 index 0000000000..c66cd46585 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java @@ -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. + * + *

The view class for all views created by this resolver can be specified + * via the {@link #setViewClass(Class)} property. + * + *

Note: 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; + } + +} diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd index 39570b69ed..671014d478 100644 --- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd +++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc-4.2.xsd @@ -973,7 +973,7 @@ element + To configure Tiles you must also add a top-level element or declare a TilesConfigurer bean. ]]> @@ -983,7 +983,7 @@ element + To configure FreeMarker you must also add a top-level element or declare a FreeMarkerConfigurer bean. ]]> @@ -993,7 +993,7 @@ element + To configure Velocity you must also add a top-level element or declare a VelocityConfigurer bean. ]]> @@ -1003,11 +1003,20 @@ element + To configure the Groovy markup template engine you must also add a top-level element or declare a GroovyMarkupConfigurer bean. ]]> + + + element + or declare a ScriptTemplateConfigurer bean. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index f9cbb1e147..5a302e19ba 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -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 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 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()); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistryTests.java index c90b5fded2..fdbf88740f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistryTests.java @@ -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(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ErbJrubyScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ErbJrubyScriptTemplateTests.java new file mode 100644 index 0000000000..ca1b5dc49f --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ErbJrubyScriptTemplateTests.java @@ -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 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("Layout example

This is the body

", + response.getContentAsString()); + } + + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map 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; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/HandlebarsNashornScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/HandlebarsNashornScriptTemplateTests.java new file mode 100644 index 0000000000..813eaf1f7a --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/HandlebarsNashornScriptTemplateTests.java @@ -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 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("Layout example

This is the body

", + response.getContentAsString()); + } + + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map 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; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/MustacheNashornScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/MustacheNashornScriptTemplateTests.java new file mode 100644 index 0000000000..b1110b5435 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/MustacheNashornScriptTemplateTests.java @@ -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 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("Layout example

This is the body

", + response.getContentAsString()); + } + + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map 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; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ReactNashornScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ReactNashornScriptTemplateTests.java new file mode 100644 index 0000000000..d42a8f6221 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ReactNashornScriptTemplateTests.java @@ -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 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("Layout example

This is the body

", + response.getContentAsString()); + } + + @Test + public void renderJsxTemplate() throws Exception { + Map 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("Layout example

This is the body

", + response.getContentAsString()); + } + + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map 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; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java new file mode 100644 index 0000000000..095447f8d1 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurerTests.java @@ -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 { + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolverTests.java new file mode 100644 index 0000000000..3712d30ad6 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolverTests.java @@ -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); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java new file mode 100644 index 0000000000..1ae173d5c9 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java @@ -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()); + + 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 configMap = new HashMap(); + 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 { + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/StringJythonScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/StringJythonScriptTemplateTests.java new file mode 100644 index 0000000000..1a710f8413 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/StringJythonScriptTemplateTests.java @@ -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 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("Layout example

This is the body

", + response.getContentAsString()); + } + + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map 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; + } + } + +} diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution-content-negotiation.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution-content-negotiation.xml index 91429a4835..54acf06c40 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution-content-negotiation.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution-content-negotiation.xml @@ -26,6 +26,7 @@ + @@ -40,5 +41,7 @@ + + \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml index 7814166aa6..3152851100 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-view-resolution.xml @@ -13,6 +13,7 @@ + @@ -33,5 +34,9 @@ + + + + \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/render.rb b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/render.rb new file mode 100644 index 0000000000..6825f3a935 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/render.rb @@ -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 \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/template.erb b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/template.erb new file mode 100644 index 0000000000..7a5f85fa96 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/erb/template.erb @@ -0,0 +1 @@ +<%= title %>

<%= body %>

\ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/polyfill.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/polyfill.js new file mode 100644 index 0000000000..14aaa26063 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/polyfill.js @@ -0,0 +1 @@ +var window = {}; \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/render.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/render.js new file mode 100644 index 0000000000..03d0166bf1 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/render.js @@ -0,0 +1,5 @@ +// TODO Manage compiled template cache +function render(template, model) { + var compiledTemplate = Handlebars.compile(template); + return compiledTemplate(model); +} \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/template.html b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/template.html new file mode 100644 index 0000000000..9110e3181b --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/handlebars/template.html @@ -0,0 +1 @@ +{{title}}

{{body}}

\ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/mustache/template.html b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/mustache/template.html new file mode 100644 index 0000000000..9110e3181b --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/mustache/template.html @@ -0,0 +1 @@ +{{title}}

{{body}}

\ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/render.py b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/render.py new file mode 100644 index 0000000000..f78ad68b8d --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/render.py @@ -0,0 +1,5 @@ +from string import Template + +def render(template, model): + s = Template(template) + return s.substitute(model) \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/template.html b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/template.html new file mode 100644 index 0000000000..e713c67e8e --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/python/template.html @@ -0,0 +1 @@ +$title

$body

\ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/polyfill.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/polyfill.js new file mode 100644 index 0000000000..52454f31c7 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/polyfill.js @@ -0,0 +1,5 @@ +var global = this; +var console = {}; +console.debug = print; +console.warn = print; +console.log = print; \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/render.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/render.js new file mode 100644 index 0000000000..fdf22ff1b1 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/render.js @@ -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); +} \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.js new file mode 100644 index 0000000000..73412bde43 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.js @@ -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))); + } +}); \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.jsx b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.jsx new file mode 100644 index 0000000000..89eec8aac9 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/react/template.jsx @@ -0,0 +1,5 @@ +React.createClass({ + render: function() { + return {this.props.title}

{this.props.body}

+ } +}); \ No newline at end of file diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index b429f41995..d9d979e012 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -4762,7 +4762,7 @@ And the same in XML: ---- -Note however that FreeMarker, Velocity, Tiles, and Groovy Markup also require +Note however that FreeMarker, Velocity, Tiles, Groovy Markup and script templates also require configuration of the underlying view technology. The MVC namespace provides dedicated elements. For example with FreeMarker: diff --git a/src/asciidoc/web-view.adoc b/src/asciidoc/web-view.adoc index 19f459c3f0..7e5a01ecd3 100644 --- a/src/asciidoc/web-view.adoc +++ b/src/asciidoc/web-view.adoc @@ -2357,6 +2357,192 @@ https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-sp +[[view-script]] +== Script templates + +It is possible to integrate any templating library running on top of a JSR-223 +script engine in web applications using Spring. The following describes in a +broad way how to do this. The script engine must implement both `ScriptEngine` +and `Invocable` interfaces. + +It has been tested with: + +* http://handlebarsjs.com/[Handlebars] running on http://openjdk.java.net/projects/nashorn/[Nashorn] +* https://mustache.github.io/[Mustache] running on http://openjdk.java.net/projects/nashorn/[Nashorn] +* http://facebook.github.io/react/[React] running on http://openjdk.java.net/projects/nashorn/[Nashorn] +* http://www.embeddedjs.com/[EJS] running on http://openjdk.java.net/projects/nashorn/[Nashorn] +* http://www.stuartellis.eu/articles/erb/[ERB] running on http://jruby.org[JRuby] +* https://docs.python.org/2/library/string.html#template-strings[String templates] running on http://www.jython.org/[Jython] + +[[view-script-dependencies]] +=== Dependencies + +To be able to use script templates integration, you need to have available in your classpath +the script engine: + +* http://openjdk.java.net/projects/nashorn/[Nashorn] Javascript engine is provided builtin with Java 8+ +* http://docs.oracle.com/javase/7/docs/technotes/guides/scripting/programmer_guide/#jsengine[Rhino] + Javascript engine is provided builtin with Java 6 and Java 7. + Please notice that using Rhino is not recommended since it does not + support running most template engines. +* http://jruby.org[JRuby] dependency should be added in order to get Ruby support. +* http://www.jython.org[Jython] dependency should be added in order to get Python support. + +You should also need to add dependencies for your script based template engine. For example, +for Javascript you can use http://www.webjars.org/[WebJars] to add Maven/Gradle dependencies +in order to make your javascript libraries available in the classpath. + + +[[view-script-integrate]] +=== How to integrate script based templating + +To be able to use script templates, you have to configure it in order to specify various parameters +like the script engine to use, the script files to load and what function should be called to +render the templates. This is done thanks to a `ScriptTemplateConfigurer` bean and optional script +files. + +For example, in order to render Mustache templates thanks to the Nashorn Javascript engine +provided with Java 8+, you should declare the following configuration: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @Configuration + @EnableWebMvc + public class MustacheConfig extends WebMvcConfigurerAdapter { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("mustache.js"); + configurer.setRenderObject("Mustache"); + configurer.setRenderFunction("render"); + return configurer; + } + } +---- + +The XML counterpart using MVC namespace is: + +[source,xml,indent=0] +[subs="verbatim,quotes"] +---- + + + + + + + + + +---- + +The controller is exactly what you should expect: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @Controller + public class SampleController { + + @RequestMapping + public ModelAndView test() { + ModelAndView mav = new ModelAndView(); + mav.addObject("title", "Sample title").addObject("body", "Sample body"); + mav.setViewName("template.html"); + return mav; + } + } +---- + +And the Mustache template is: + +[source,html,indent=0] +[subs="verbatim,quotes"] +---- + + + {{title}} + + +

{{body}}

+ + +---- + +The render function is called with the following parameters: + +* template: the view template content (String) +* model: the view model (Map) + +`Mustache.render()` is natively compatible with this signature, so you can call it directly. + +If your templating technology requires some customization, you may provide a script that +implements a custom render function. For example, http://handlebarsjs.com[Handlerbars] +needs to compile templates before using them, and requires a +http://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some +browser facilities not available in the server-side script engine. + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @Configuration + @EnableWebMvc + public class MustacheConfig extends WebMvcConfigurerAdapter { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); + configurer.setRenderFunction("render"); + return configurer; + } + } +---- + +`polyfill.js` only defines the `window` object needed by Handlebars to run properly: + +[source,javascript,indent=0] +[subs="verbatim,quotes"] +---- + var window = {}; +---- + +This basic `render.js` implementation compiles the template before using it. A production +ready implementation should also store and reused cached templates / pre-compiled templates. +This can be done on the script side, as well as any customization you need (managing +template engine configuration for example). + +[source,javascript,indent=0] +[subs="verbatim,quotes"] +---- + function render(template, model) { + var compiledTemplate = Handlebars.compile(template); + return compiledTemplate(model); + } +---- + +Check out Spring script templates unit tests +(https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[java], +https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources]) +for more configuration examples. + + + + [[view-xml-marshalling]] == XML Marshalling View The `MarshallingView` uses an XML `Marshaller` defined in the `org.springframework.oxm`