diff --git a/build.gradle b/build.gradle index 7d8040d97c..7763df2eb2 100644 --- a/build.gradle +++ b/build.gradle @@ -865,6 +865,8 @@ project("spring-web-reactive") { testRuntime("org.jboss.xnio:xnio-nio:${xnioVersion}") testRuntime("org.jboss.logging:jboss-logging:3.3.0.Final") testRuntime("org.webjars:underscorejs:1.8.3") + testRuntime("org.jruby:jruby:9.1.6.0") + testRuntime("org.python:jython-standalone:2.5.3") } if (JavaVersion.current().java9Compatible) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java new file mode 100644 index 0000000000..7bbd83497e --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import java.nio.charset.Charset; +import javax.script.ScriptEngine; + +/** + * Interface to be implemented by objects that configure and manage a + * JSR-223 {@link ScriptEngine} for automatic lookup in a web environment. + * Detected and used by {@link ScriptTemplateView}. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public interface ScriptTemplateConfig { + + /** + * Return the {@link ScriptEngine} to use by the views. + */ + ScriptEngine getEngine(); + + /** + * Return the engine name that will be used to instantiate the {@link ScriptEngine}. + */ + String getEngineName(); + + /** + * Return whether to use a shared engine for all threads or whether to create + * thread-local engine instances for each thread. + */ + Boolean isSharedEngine(); + + /** + * Return the scripts to be loaded by the script engine (library or user provided). + */ + String[] getScripts(); + + /** + * Return the object where the render function belongs (optional). + */ + String getRenderObject(); + + /** + * Return the render function name (mandatory). + */ + String getRenderFunction(); + + /** + * Return the charset used to read script and template files. + */ + Charset getCharset(); + + /** + * Return the resource loader path(s) via a Spring resource location. + */ + String getResourceLoaderPath(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java new file mode 100644 index 0000000000..2eafab7fdd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import java.nio.charset.Charset; +import javax.script.ScriptEngine; + +/** + * 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;
+ * }
+ * 
+ * + *

NOTE: It is possible to use non thread-safe script engines with + * templating libraries not designed for concurrency, like Handlebars or React running on + * Nashorn, by setting the {@link #setSharedEngine sharedEngine} property to {@code false}. + * + * @author Sebastien Deleuze + * @since 5.0 + * @see ScriptTemplateView + */ +public class ScriptTemplateConfigurer implements ScriptTemplateConfig { + + private ScriptEngine engine; + + private String engineName; + + private Boolean sharedEngine; + + private String[] scripts; + + private String renderObject; + + private String renderFunction; + + private Charset charset; + + private String resourceLoaderPath; + + + /** + * 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. + *

When the {@code sharedEngine} flag is set to {@code false}, you should not specify + * the script engine with this setter, but with the {@link #setEngineName(String)} + * one (since it implies multiple lazy instantiations of the script engine). + * @see #setEngineName(String) + */ + public void setEngine(ScriptEngine 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. + * @see #setEngine(ScriptEngine) + */ + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + @Override + public String getEngineName() { + return this.engineName; + } + + /** + * When set to {@code false}, a new {@link ScriptEngine} instance will be created + * for each request, else the same instance will be reused. + * This flag should be set to {@code false} for those using non thread-safe script + * engines with templating libraries not designed for + * concurrency, like Handlebars or React running on Nashorn for example. + *

When this flag is set to {@code false}, the script engine must be specified using + * {@link #setEngineName(String)}. Using {@link #setEngine(ScriptEngine)} is not + * possible because multiple instances of the script engine need to be created for + * each request. + * @see THREADING ScriptEngine parameter + */ + public void setSharedEngine(Boolean sharedEngine) { + this.sharedEngine = sharedEngine; + } + + @Override + public Boolean isSharedEngine() { + return this.sharedEngine; + } + + /** + * 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 + * @see WebJars + */ + public void setScripts(String... scriptNames) { + this.scripts = scriptNames; + } + + @Override + public String[] getScripts() { + return this.scripts; + } + + /** + * Set the object where the render function belongs (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 getRenderObject() { + return this.renderObject; + } + + /** + * Set the render function name (mandatory). + * + *

This function will be called with the following parameters: + *

    + *
  1. {@code String template}: the template content
  2. + *
  3. {@code Map model}: the view model
  4. + *
  5. {@code String url}: the template url
  6. + *
+ */ + public void setRenderFunction(String renderFunction) { + this.renderFunction = renderFunction; + } + + @Override + public String getRenderFunction() { + return this.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; + } + + @Override + public String getResourceLoaderPath() { + return this.resourceLoaderPath; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java new file mode 100644 index 0000000000..ebf11348aa --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java @@ -0,0 +1,319 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +import reactor.core.publisher.Mono; + +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.core.io.buffer.DataBuffer; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.scripting.support.StandardScriptEvalException; +import org.springframework.scripting.support.StandardScriptUtils; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.result.view.AbstractUrlBasedView; +import org.springframework.web.server.ServerWebExchange; + +/** + * An {@link AbstractUrlBasedView} subclass designed to run any template library + * based on a JSR-223 script engine. + * + *

If not set, each property is auto-detected by looking up a single + * {@link ScriptTemplateConfig} bean in the web application context and using + * it to obtain the configured properties. + * + *

Nashorn Javascript engine requires Java 8+, and may require setting the + * {@code sharedEngine} property to {@code false} in order to run properly. See + * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 5.0 + * @see ScriptTemplateConfigurer + * @see ScriptTemplateViewResolver + */ +public class ScriptTemplateView extends AbstractUrlBasedView { + + private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:"; + + + private ScriptEngine engine; + + private String engineName; + + private Boolean sharedEngine; + + private String[] scripts; + + private String renderObject; + + private String renderFunction; + + private String[] resourceLoaderPaths; + + private ResourceLoader resourceLoader; + + private volatile ScriptEngineManager scriptEngineManager; + + + /** + * Constructor for use as a bean. + * @see #setUrl + */ + public ScriptTemplateView() { + } + + /** + * Create a new ScriptTemplateView with the given URL. + */ + public ScriptTemplateView(String url) { + super(url); + } + + + /** + * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation. + */ + public void setEngine(ScriptEngine engine) { + Assert.isInstanceOf(Invocable.class, engine); + this.engine = engine; + } + + /** + * See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation. + */ + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + /** + * See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation. + */ + public void setSharedEngine(Boolean sharedEngine) { + this.sharedEngine = sharedEngine; + } + + /** + * See {@link ScriptTemplateConfigurer#setScripts(String...)} documentation. + */ + public void setScripts(String... scripts) { + this.scripts = scripts; + } + + /** + * See {@link ScriptTemplateConfigurer#setRenderObject(String)} documentation. + */ + public void setRenderObject(String renderObject) { + this.renderObject = renderObject; + } + + /** + * See {@link ScriptTemplateConfigurer#setRenderFunction(String)} documentation. + */ + public void setRenderFunction(String functionName) { + this.renderFunction = functionName; + } + + /** + * See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation. + */ + public void setResourceLoaderPath(String resourceLoaderPath) { + String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath); + this.resourceLoaderPaths = new String[paths.length + 1]; + this.resourceLoaderPaths[0] = ""; + for (int i = 0; i < paths.length; i++) { + String path = paths[i]; + if (!path.endsWith("/") && !path.endsWith(":")) { + path = path + "/"; + } + this.resourceLoaderPaths[i + 1] = path; + } + } + + @Override + public void setApplicationContext(ApplicationContext context) { + super.setApplicationContext(context); + + ScriptTemplateConfig viewConfig = autodetectViewConfig(); + if (this.engine == null && viewConfig.getEngine() != null) { + setEngine(viewConfig.getEngine()); + } + if (this.engineName == null && viewConfig.getEngineName() != null) { + this.engineName = viewConfig.getEngineName(); + } + if (this.scripts == null && viewConfig.getScripts() != null) { + this.scripts = viewConfig.getScripts(); + } + if (this.renderObject == null && viewConfig.getRenderObject() != null) { + this.renderObject = viewConfig.getRenderObject(); + } + if (this.renderFunction == null && viewConfig.getRenderFunction() != null) { + this.renderFunction = viewConfig.getRenderFunction(); + } + if (viewConfig.getCharset() != null) { + setDefaultCharset(viewConfig.getCharset()); + } + if (this.resourceLoaderPaths == null) { + String resourceLoaderPath = viewConfig.getResourceLoaderPath(); + setResourceLoaderPath(resourceLoaderPath == null ? DEFAULT_RESOURCE_LOADER_PATH : resourceLoaderPath); + } + if (this.resourceLoader == null) { + this.resourceLoader = getApplicationContext(); + } + if (this.sharedEngine == null && viewConfig.isSharedEngine() != null) { + this.sharedEngine = viewConfig.isSharedEngine(); + } + + Assert.isTrue(!(this.engine != null && this.engineName != null), + "You should define either 'engine' or 'engineName', not both."); + Assert.isTrue(!(this.engine == null && this.engineName == null), + "No script engine found, please specify either 'engine' or 'engineName'."); + + if (Boolean.FALSE.equals(this.sharedEngine)) { + Assert.isTrue(this.engineName != null, + "When 'sharedEngine' is set to false, you should specify the " + + "script engine using the 'engineName' property, not the 'engine' one."); + } + else if (this.engine != null) { + loadScripts(this.engine); + } + else { + setEngine(createEngineFromName()); + } + + Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); + } + + protected ScriptEngine getEngine() { + return Boolean.FALSE.equals(this.sharedEngine) ? createEngineFromName() : this.engine; + } + + protected ScriptEngine createEngineFromName() { + if (this.scriptEngineManager == null) { + this.scriptEngineManager = new ScriptEngineManager(getApplicationContext().getClassLoader()); + } + + ScriptEngine engine = StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName); + loadScripts(engine); + return engine; + } + + protected void loadScripts(ScriptEngine engine) { + if (!ObjectUtils.isEmpty(this.scripts)) { + for (String script : this.scripts) { + Resource resource = getResource(script); + if (resource == null) { + throw new IllegalStateException("Script resource [" + script + "] not found"); + } + try { + engine.eval(new InputStreamReader(resource.getInputStream())); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to evaluate script [" + script + "]", ex); + } + } + } + } + + protected Resource getResource(String location) { + for (String path : this.resourceLoaderPaths) { + Resource resource = this.resourceLoader.getResource(path + location); + if (resource.exists()) { + return resource; + } + } + return null; + } + + 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 " + + "web application context or the parent root context: ScriptTemplateConfigurer is " + + "the usual implementation. This bean may have any name.", ex); + } + } + + @Override + public boolean checkResourceExists(Locale locale) throws Exception { + return (getResource(getUrl()) != null); + } + + @Override + protected Mono renderInternal(Map model, MediaType contentType, ServerWebExchange exchange) { + return Mono.defer(() -> { + ServerHttpResponse response = exchange.getResponse(); + try { + ScriptEngine engine = getEngine(); + Invocable invocable = (Invocable) engine; + String url = getUrl(); + String template = getTemplate(url); + + Object html; + if (this.renderObject != null) { + Object thiz = engine.eval(this.renderObject); + html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url); + } + else { + html = invocable.invokeFunction(this.renderFunction, template, model, url); + } + + byte[] bytes = String.valueOf(html).getBytes(StandardCharsets.UTF_8); + DataBuffer buffer = response.bufferFactory().allocateBuffer(bytes.length).write(bytes); + return response.writeWith(Mono.just(buffer)); + } + catch (ScriptException ex) { + throw new IllegalStateException("Failed to render script template", new StandardScriptEvalException(ex)); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to render script template", ex); + } + }); + } + + protected String getTemplate(String path) throws IOException { + Resource resource = getResource(path); + if (resource == null) { + throw new IllegalStateException("Template resource [" + path + "] not found"); + } + InputStreamReader reader = new InputStreamReader(resource.getInputStream(), getDefaultCharset()); + return FileCopyUtils.copyToString(reader); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolver.java new file mode 100644 index 0000000000..d24643929f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolver.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import org.springframework.web.reactive.result.view.UrlBasedViewResolver; + +/** + * Convenience subclass of {@link 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 5.0 + * @see ScriptTemplateConfigurer + */ +public class ScriptTemplateViewResolver extends UrlBasedViewResolver { + + /** + * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}: + * by default {@link ScriptTemplateView}. + */ + public ScriptTemplateViewResolver() { + setViewClass(requiredViewClass()); + } + + /** + * A convenience constructor that allows for specifying {@link #setPrefix prefix} + * and {@link #setSuffix suffix} as constructor arguments. + * @param prefix the prefix that gets prepended to view names when building a URL + * @param suffix the suffix that gets appended to view names when building a URL + */ + public ScriptTemplateViewResolver(String prefix, String suffix) { + this(); + setPrefix(prefix); + setSuffix(suffix); + } + + @Override + protected Class requiredViewClass() { + return ScriptTemplateView.class; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java new file mode 100644 index 0000000000..5ad2ee25a4 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for views based on the JSR-223 script engine abstraction + * (as included in Java 6+), e.g. using JavaScript via Nashorn on JDK 8. + * Contains a View implementation for scripted templates. + */ +package org.springframework.web.reactive.result.view.script; diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JRubyScriptTemplateTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JRubyScriptTemplateTests.java new file mode 100644 index 0000000000..b1f8c692e4 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JRubyScriptTemplateTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Unit tests for ERB templates running on JRuby. + * + * @author Sebastien Deleuze + */ +@Ignore("JRuby not compatible with JDK 9 yet") +public class JRubyScriptTemplateTests { + + private StaticApplicationContext context; + + @Before + public void setup() { + this.context = new StaticApplicationContext(); + } + + @Test + public void renderTemplate() throws Exception { + Map model = new HashMap<>(); + model.put("title", "Layout example"); + model.put("body", "This is the body"); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/jruby/template.erb", model); + assertEquals("Layout example

This is the body

", + response.getBodyAsString().block()); + } + + private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model) throws Exception { + ScriptTemplateView view = createViewWithUrl(viewUrl); + MockServerHttpRequest request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + view.renderInternal(model, MediaType.TEXT_HTML, exchange).block(); + 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/reactive/result/view/script/jruby/render.rb"); + configurer.setEngineName("jruby"); + configurer.setRenderFunction("render"); + return configurer; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JythonScriptTemplateTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JythonScriptTemplateTests.java new file mode 100644 index 0000000000..b08e0aa701 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/JythonScriptTemplateTests.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.reactive.result.view.script; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Unit tests for String templates running on Jython. + * + * @author Sebastien Deleuze + */ +public class JythonScriptTemplateTests { + + private StaticApplicationContext context; + + + @Before + public void setup() { + this.context = new StaticApplicationContext(); + } + + @Test + public void renderTemplate() throws Exception { + Map model = new HashMap<>(); + model.put("title", "Layout example"); + model.put("body", "This is the body"); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/jython/template.html", model); + assertEquals("Layout example

This is the body

", + response.getBodyAsString().block()); + } + + private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model) throws Exception { + ScriptTemplateView view = createViewWithUrl(viewUrl); + MockServerHttpRequest request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + view.renderInternal(model, MediaType.TEXT_HTML, exchange).block(); + 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/reactive/result/view/script/jython/render.py"); + configurer.setEngineName("jython"); + configurer.setRenderFunction("render"); + return configurer; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java new file mode 100644 index 0000000000..acede99e9c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java @@ -0,0 +1,101 @@ +/* + * 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.reactive.result.view.script; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Unit tests for Kotlin script templates running on Kotlin JSR 223 support + * + * @author Sebastien Deleuze + */ +@Ignore // Temporary disabled since Kotlin 1.1-M04 generates bytecode not Kotlin 1.0 compliant, will be enable as soon as Kotlin 1.1-M05 is available +public class KotlinScriptTemplateTests { + + private StaticApplicationContext context; + + @Before + public void setup() { + this.context = new StaticApplicationContext(); + } + + @Test + public void renderTemplate() throws Exception { + Map model = new HashMap<>(); + model.put("title", "Layout example"); + model.put("body", "This is the body"); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/template.kts", + model, ScriptTemplatingConfiguration.class); + assertEquals("Layout example

This is the body

", + response.getBodyAsString().block()); + } + + private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model, Class configuration) throws Exception { + ScriptTemplateView view = createViewWithUrl(viewUrl, configuration); + MockServerHttpRequest request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + view.renderInternal(model, MediaType.TEXT_HTML, exchange).block(); + return response; + } + + private ScriptTemplateView createViewWithUrl(String viewUrl, Class configuration) throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(configuration); + ctx.refresh(); + + ScriptTemplateView view = new ScriptTemplateView(); + view.setApplicationContext(ctx); + view.setUrl(viewUrl); + view.afterPropertiesSet(); + return view; + } + + + @Configuration + static class ScriptTemplatingConfiguration { + + @Bean + public ScriptTemplateConfigurer kotlinScriptConfigurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("kotlin"); + configurer.setScripts("org/springframework/web/reactive/result/view/script/kotlin/render.kts"); + configurer.setRenderFunction("render"); + return configurer; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/NashornScriptTemplateTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/NashornScriptTemplateTests.java new file mode 100644 index 0000000000..2cc17ef821 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/NashornScriptTemplateTests.java @@ -0,0 +1,121 @@ +/* + * 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.reactive.result.view.script; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +/** + * Unit tests for pure Javascript templates running on Nashorn engine. + * + * @author Sebastien Deleuze + */ +public class NashornScriptTemplateTests { + + private StaticApplicationContext context; + + @Before + public void setup() { + this.context = new StaticApplicationContext(); + } + + @Test + public void renderTemplate() throws Exception { + Map model = new HashMap<>(); + model.put("title", "Layout example"); + model.put("body", "This is the body"); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/nashorn/template.html", + model, ScriptTemplatingConfiguration.class); + assertEquals("Layout example

This is the body

", + response.getBodyAsString().block()); + } + + @Test // SPR-13453 + public void renderTemplateWithUrl() throws Exception { + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/nashorn/template.html", + null, ScriptTemplatingWithUrlConfiguration.class); + assertEquals("Check url parameter

org/springframework/web/reactive/result/view/script/nashorn/template.html

", + response.getBodyAsString().block()); + } + + private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model, Class configuration) throws Exception { + ScriptTemplateView view = createViewWithUrl(viewUrl, configuration); + MockServerHttpRequest request = new MockServerHttpRequest(); + MockServerHttpResponse response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + view.renderInternal(model, MediaType.TEXT_HTML, exchange).block(); + return response; + } + + private ScriptTemplateView createViewWithUrl(String viewUrl, Class configuration) throws Exception { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(configuration); + ctx.refresh(); + + ScriptTemplateView view = new ScriptTemplateView(); + view.setApplicationContext(ctx); + view.setUrl(viewUrl); + view.afterPropertiesSet(); + return view; + } + + + @Configuration + static class ScriptTemplatingConfiguration { + + @Bean + public ScriptTemplateConfigurer nashornConfigurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("org/springframework/web/reactive/result/view/script/nashorn/render.js"); + configurer.setRenderFunction("render"); + return configurer; + } + } + + + @Configuration + static class ScriptTemplatingWithUrlConfiguration { + + @Bean + public ScriptTemplateConfigurer nashornConfigurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("org/springframework/web/reactive/result/view/script/nashorn/render.js"); + configurer.setRenderFunction("renderWithUrl"); + return configurer; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolverTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolverTests.java new file mode 100644 index 0000000000..81103731f2 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewResolverTests.java @@ -0,0 +1,40 @@ +/* + * 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.reactive.result.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-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java new file mode 100644 index 0000000000..529d79be01 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2016 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.reactive.result.view.script; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.script.Invocable; +import javax.script.ScriptEngine; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import static org.mockito.BDDMockito.*; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.support.StaticApplicationContext; + +/** + * Unit tests for {@link ScriptTemplateView}. + * + * @author Sebastien Deleuze + */ +public class ScriptTemplateViewTests { + + private ScriptTemplateView view; + + private ScriptTemplateConfigurer configurer; + + private StaticApplicationContext context; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + + @Before + public void setup() { + this.configurer = new ScriptTemplateConfigurer(); + this.context = new StaticApplicationContext(); + this.context.getBeanFactory().registerSingleton("scriptTemplateConfigurer", this.configurer); + this.view = new ScriptTemplateView(); + } + + + @Test + public void missingTemplate() throws Exception { + this.context.refresh(); + this.view.setResourceLoaderPath("classpath:org/springframework/web/reactive/result/view/script/"); + this.view.setUrl("missing.txt"); + this.view.setEngine(mock(InvocableScriptEngine.class)); + this.configurer.setRenderFunction("render"); + this.view.setApplicationContext(this.context); + assertFalse(this.view.checkResourceExists(Locale.ENGLISH)); + } + + @Test + public void missingScriptTemplateConfig() throws Exception { + this.expectedException.expect(ApplicationContextException.class); + this.view.setApplicationContext(new StaticApplicationContext()); + this.expectedException.expectMessage(contains("ScriptTemplateConfig")); + } + + @Test + public void detectScriptTemplateConfigWithEngine() { + InvocableScriptEngine engine = mock(InvocableScriptEngine.class); + this.configurer.setEngine(engine); + this.configurer.setRenderObject("Template"); + this.configurer.setRenderFunction("render"); + this.configurer.setCharset(StandardCharsets.ISO_8859_1); + this.configurer.setSharedEngine(true); + + DirectFieldAccessor accessor = new DirectFieldAccessor(this.view); + this.view.setApplicationContext(this.context); + assertEquals(engine, accessor.getPropertyValue("engine")); + assertEquals("Template", accessor.getPropertyValue("renderObject")); + assertEquals("render", accessor.getPropertyValue("renderFunction")); + assertEquals(StandardCharsets.ISO_8859_1, accessor.getPropertyValue("defaultCharset")); + assertEquals(true, accessor.getPropertyValue("sharedEngine")); + } + + @Test + public void detectScriptTemplateConfigWithEngineName() { + this.configurer.setEngineName("nashorn"); + this.configurer.setRenderObject("Template"); + this.configurer.setRenderFunction("render"); + + DirectFieldAccessor accessor = new DirectFieldAccessor(this.view); + this.view.setApplicationContext(this.context); + assertEquals("nashorn", accessor.getPropertyValue("engineName")); + assertNotNull(accessor.getPropertyValue("engine")); + assertEquals("Template", accessor.getPropertyValue("renderObject")); + assertEquals("render", accessor.getPropertyValue("renderFunction")); + assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("defaultCharset")); + } + + @Test + public void customEngineAndRenderFunction() throws Exception { + ScriptEngine engine = mock(InvocableScriptEngine.class); + given(engine.get("key")).willReturn("value"); + this.view.setEngine(engine); + this.view.setRenderFunction("render"); + this.view.setApplicationContext(this.context); + engine = this.view.getEngine(); + assertNotNull(engine); + assertEquals("value", engine.get("key")); + DirectFieldAccessor accessor = new DirectFieldAccessor(this.view); + assertNull(accessor.getPropertyValue("renderObject")); + assertEquals("render", accessor.getPropertyValue("renderFunction")); + assertEquals(StandardCharsets.UTF_8, accessor.getPropertyValue("defaultCharset")); + } + + @Test + public void nonSharedEngine() throws Exception { + int iterations = 20; + this.view.setEngineName("nashorn"); + this.view.setRenderFunction("render"); + this.view.setSharedEngine(false); + this.view.setApplicationContext(this.context); + ExecutorService executor = Executors.newFixedThreadPool(4); + List> results = new ArrayList<>(); + for (int i = 0; i < iterations; i++) { + results.add(executor.submit(() -> view.getEngine() != null)); + } + assertEquals(iterations, results.size()); + for (int i = 0; i < iterations; i++) { + assertTrue(results.get(i).get()); + } + executor.shutdown(); + } + + @Test + public void nonInvocableScriptEngine() throws Exception { + this.expectedException.expect(IllegalArgumentException.class); + this.view.setEngine(mock(ScriptEngine.class)); + this.expectedException.expectMessage(contains("instance")); + } + + @Test + public void noRenderFunctionDefined() { + this.view.setEngine(mock(InvocableScriptEngine.class)); + this.expectedException.expect(IllegalArgumentException.class); + this.view.setApplicationContext(this.context); + this.expectedException.expectMessage(contains("renderFunction")); + } + + @Test + public void engineAndEngineNameBothDefined() { + this.view.setEngine(mock(InvocableScriptEngine.class)); + this.view.setEngineName("test"); + this.view.setRenderFunction("render"); + this.expectedException.expect(IllegalArgumentException.class); + this.view.setApplicationContext(this.context); + this.expectedException.expectMessage(contains("'engine' or 'engineName'")); + } + + @Test + public void engineSetterAndNonSharedEngine() { + this.view.setEngine(mock(InvocableScriptEngine.class)); + this.view.setRenderFunction("render"); + this.view.setSharedEngine(false); + this.expectedException.expect(IllegalArgumentException.class); + this.view.setApplicationContext(this.context); + this.expectedException.expectMessage(contains("sharedEngine")); + } + + private interface InvocableScriptEngine extends ScriptEngine, Invocable { + } + +} diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/empty.txt b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/empty.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/render.rb b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/render.rb new file mode 100644 index 0000000000..e5db6520d5 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/render.rb @@ -0,0 +1,20 @@ +require 'erb' +require 'ostruct' +require 'java' + +# Renders an ERB template against a hashmap of variables. +def render(template, variables, url) + 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-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/template.erb b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/template.erb new file mode 100644 index 0000000000..7a5f85fa96 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/jruby/template.erb @@ -0,0 +1 @@ +<%= title %>

<%= body %>

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

$body

\ No newline at end of file diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/render.kts b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/render.kts new file mode 100644 index 0000000000..d1ab1ae6db --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/render.kts @@ -0,0 +1,10 @@ +import javax.script.* + +// TODO Use engine.eval(String, Bindings) when https://youtrack.jetbrains.com/issue/KT-15450 will be fixed +fun render(template: String, model: Map, url: String): String { + val engine = ScriptEngineManager().getEngineByName("kotlin") + val bindings = SimpleBindings() + bindings.putAll(model) + engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE) + return engine.eval(template) as String +} diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/template.kts b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/template.kts new file mode 100644 index 0000000000..bacaa3f3d0 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/template.kts @@ -0,0 +1 @@ +"""${bindings["title"]}

${bindings["body"]}

""" diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/render.js b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/render.js new file mode 100644 index 0000000000..8b6a712028 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/render.js @@ -0,0 +1,7 @@ +function render(template, model) { + return template.replace("{{title}}", model.title).replace("{{body}}", model.body); +} + +function renderWithUrl(template, model, url) { + return template.replace("{{title}}", "Check url parameter").replace("{{body}}", url); +} diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/template.html b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/template.html new file mode 100644 index 0000000000..9110e3181b --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/nashorn/template.html @@ -0,0 +1 @@ +{{title}}

{{body}}

\ No newline at end of file