From d5f9ad03a78ded2da778ae9d63b29e1a7bdd6359 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 16 Jun 2017 10:26:26 +0200 Subject: [PATCH] Support ScriptEngine#eval(String, Bindings) in ScriptTemplateView Supporting ScriptEngine#eval(String, Bindings) when no render function is specified allows to support use cases where script templates are simply evaluating a script expression with an even more simplified configuration. This improvement also makes it possible to use script engines that do not implement Invocable. Issue: SPR-15115 --- .../view/script/ScriptTemplateConfig.java | 5 +++- .../view/script/ScriptTemplateConfigurer.java | 27 +++++++++++++++---- .../view/script/ScriptTemplateView.java | 20 +++++++++----- .../script/KotlinScriptTemplateTests.java | 23 ++++++++++++++++ .../view/script/ScriptTemplateViewTests.java | 9 +++---- .../result/view/script/kotlin/eval.kts | 4 +++ .../view/script/ScriptTemplateConfig.java | 5 +++- .../view/script/ScriptTemplateConfigurer.java | 26 +++++++++++++++--- .../view/script/ScriptTemplateView.java | 19 ++++++++----- .../script/KotlinScriptTemplateTests.java | 22 +++++++++++++++ .../view/script/ScriptTemplateViewTests.java | 9 +++---- .../web/servlet/view/script/kotlin/eval.kts | 4 +++ 12 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java index 1b494b5f799..8d1cac3a43f 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java @@ -17,6 +17,8 @@ package org.springframework.web.reactive.result.view.script; import java.nio.charset.Charset; + +import javax.script.Bindings; import javax.script.ScriptEngine; import org.springframework.lang.Nullable; @@ -63,7 +65,8 @@ public interface ScriptTemplateConfig { String getRenderObject(); /** - * Return the render function name (mandatory). + * Return the render function name (optional). If not specified, the script templates + * will be evaluated with {@link ScriptEngine#eval(String, Bindings)}. */ @Nullable String getRenderFunction(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java index eee09342f13..868b9532cc4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java @@ -17,6 +17,8 @@ package org.springframework.web.reactive.result.view.script; import java.nio.charset.Charset; + +import javax.script.Bindings; import javax.script.ScriptEngine; /** @@ -63,9 +65,23 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { private String resourceLoaderPath; + /** + * Default constructor. + */ + public ScriptTemplateConfigurer() { + } + + /** + * Create a new ScriptTemplateConfigurer using the given engine name. + */ + public ScriptTemplateConfigurer(String engineName) { + this.engineName = engineName; + } + + /** * Set the {@link ScriptEngine} to use by the view. - * The script engine must implement {@code Invocable}. + * If {@code renderFunction} is specified, 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)} @@ -83,7 +99,7 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { /** * Set the engine name that will be used to instantiate the {@link ScriptEngine}. - * The script engine must implement {@code Invocable}. + * If {@code renderFunction} is specified, the script engine must implement {@code Invocable}. * You must define {@code engine} or {@code engineName}, not both. * @see #setEngine(ScriptEngine) */ @@ -152,14 +168,15 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { } /** - * Set the render function name (mandatory). - * + * Set the render function name (optional). If not specified, the script templates + * will be evaluated with {@link ScriptEngine#eval(String, Bindings)}. *

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. + *
  7. {@code RenderingContext context}: the rendering context (since 5.0)
  8. *
+ * @see RenderingContext */ public void setRenderFunction(String renderFunction) { this.renderFunction = renderFunction; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java index d695eebce2f..c6f0787a7e3 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java @@ -26,6 +26,7 @@ import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import javax.script.SimpleBindings; import reactor.core.publisher.Mono; @@ -112,7 +113,6 @@ public class ScriptTemplateView extends AbstractUrlBasedView { * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation. */ public void setEngine(ScriptEngine engine) { - Assert.isInstanceOf(Invocable.class, engine, "ScriptEngine must implement Invocable"); this.engine = engine; } @@ -225,7 +225,9 @@ public class ScriptTemplateView extends AbstractUrlBasedView { setEngine(createEngineFromName()); } - Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); + if (this.renderFunction != null && this.engine != null) { + Assert.isInstanceOf(Invocable.class, this.engine, "ScriptEngine must implement Invocable when 'renderFunction' is specified."); + } } protected ScriptEngine getEngine() { @@ -297,8 +299,6 @@ public class ScriptTemplateView extends AbstractUrlBasedView { ServerHttpResponse response = exchange.getResponse(); try { ScriptEngine engine = getEngine(); - Invocable invocable = (Invocable) engine; - String url = getUrl(); Assert.state(url != null, "'url' not set"); String template = getTemplate(url); @@ -316,12 +316,18 @@ public class ScriptTemplateView extends AbstractUrlBasedView { obtainApplicationContext(), this.locale, templateLoader, url); Object html; - if (this.renderObject != null) { + if (this.renderFunction == null) { + SimpleBindings bindings = new SimpleBindings(); + bindings.putAll(model); + model.put("renderingContext", context); + html = engine.eval(template, bindings); + } + else if (this.renderObject != null) { Object thiz = engine.eval(this.renderObject); - html = invocable.invokeMethod(thiz, this.renderFunction, template, model, context); + html = ((Invocable)engine).invokeMethod(thiz, this.renderFunction, template, model, context); } else { - html = invocable.invokeFunction(this.renderFunction, template, model, context); + html = ((Invocable)engine).invokeFunction(this.renderFunction, template, model, context); } byte[] bytes = String.valueOf(html).getBytes(StandardCharsets.UTF_8); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java index 7844dcfec21..b38e6cca460 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java @@ -30,6 +30,7 @@ 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.mock.http.server.reactive.test.MockServerWebExchange; +import org.springframework.mock.web.test.MockHttpServletResponse; import static org.junit.Assert.assertEquals; @@ -60,6 +61,19 @@ public class KotlinScriptTemplateTests { response.getBodyAsString().block()); } + @Test + public void renderTemplateWithoutRenderFunction() throws Exception { + Map model = new HashMap<>(); + model.put("header", ""); + model.put("hello", "Hello"); + model.put("foo", "Foo"); + model.put("footer", ""); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/eval.kts", + model, Locale.ENGLISH, ScriptTemplatingConfigurationWithoutRenderFunction.class); + assertEquals("\n

Hello Foo

\n", + response.getBodyAsString().block()); + } + private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model, Locale locale, Class configuration) throws Exception { ScriptTemplateView view = createViewWithUrl(viewUrl, configuration); view.setLocale(locale); @@ -101,4 +115,13 @@ public class KotlinScriptTemplateTests { } } + @Configuration + static class ScriptTemplatingConfigurationWithoutRenderFunction { + + @Bean + public ScriptTemplateConfigurer kotlinScriptConfigurer() { + return new ScriptTemplateConfigurer("kotlin"); + } + } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java index 529d79be01b..1e3ca39cae4 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java @@ -151,17 +151,16 @@ public class ScriptTemplateViewTests { @Test public void nonInvocableScriptEngine() throws Exception { - this.expectedException.expect(IllegalArgumentException.class); this.view.setEngine(mock(ScriptEngine.class)); - this.expectedException.expectMessage(contains("instance")); + this.view.setApplicationContext(this.context); } @Test - public void noRenderFunctionDefined() { - this.view.setEngine(mock(InvocableScriptEngine.class)); + public void nonInvocableScriptEngineWithRenderFunction() throws Exception { + this.view.setEngine(mock(ScriptEngine.class)); + this.view.setRenderFunction("render"); this.expectedException.expect(IllegalArgumentException.class); this.view.setApplicationContext(this.context); - this.expectedException.expectMessage(contains("renderFunction")); } @Test diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts new file mode 100644 index 00000000000..6a8121acf02 --- /dev/null +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts @@ -0,0 +1,4 @@ +// TODO Improve syntax when KT-15125 will be fixed +"""${bindings["header"]} +

${bindings["hello"]} ${bindings["foo"]}

+${bindings["footer"]}""" 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 index 861f9998806..ae9935b9bbc 100644 --- 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 @@ -17,6 +17,8 @@ package org.springframework.web.servlet.view.script; import java.nio.charset.Charset; + +import javax.script.Bindings; import javax.script.ScriptEngine; import org.springframework.lang.Nullable; @@ -63,7 +65,8 @@ public interface ScriptTemplateConfig { String getRenderObject(); /** - * Return the render function name (mandatory). + * Return the render function name (optional). If not specified, the script templates + * will be evaluated with {@link ScriptEngine#eval(String, Bindings)}. */ @Nullable String getRenderFunction(); 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 index 4542c418850..d439ec788ae 100644 --- 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 @@ -17,6 +17,8 @@ package org.springframework.web.servlet.view.script; import java.nio.charset.Charset; + +import javax.script.Bindings; import javax.script.ScriptEngine; /** @@ -65,9 +67,23 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { private String resourceLoaderPath; + /** + * Default constructor. + */ + public ScriptTemplateConfigurer() { + } + + /** + * Create a new ScriptTemplateConfigurer using the given engine name. + */ + public ScriptTemplateConfigurer(String engineName) { + this.engineName = engineName; + } + + /** * Set the {@link ScriptEngine} to use by the view. - * The script engine must implement {@code Invocable}. + * If {@code renderFunction} is specified, 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)} @@ -85,7 +101,7 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { /** * Set the engine name that will be used to instantiate the {@link ScriptEngine}. - * The script engine must implement {@code Invocable}. + * If {@code renderFunction} is specified, the script engine must implement {@code Invocable}. * You must define {@code engine} or {@code engineName}, not both. * @see #setEngine(ScriptEngine) */ @@ -155,13 +171,15 @@ public class ScriptTemplateConfigurer implements ScriptTemplateConfig { } /** - * Set the render function name (mandatory). + * Set the render function name (optional). If not specified, the script templates + * will be evaluated with {@link ScriptEngine#eval(String, Bindings)}. *

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 (since 4.2.2)
  6. + *
  7. {@code RenderingContext context}: the rendering context (since 5.0)
  8. *
+ * @see RenderingContext */ public void setRenderFunction(String renderFunction) { this.renderFunction = renderFunction; 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 index 0306e3fc97b..c44ca67c1cf 100644 --- 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 @@ -29,6 +29,7 @@ import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import javax.script.SimpleBindings; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -126,7 +127,6 @@ public class ScriptTemplateView extends AbstractUrlBasedView { * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation. */ public void setEngine(ScriptEngine engine) { - Assert.isInstanceOf(Invocable.class, engine, "ScriptEngine must implement Invocable"); this.engine = engine; } @@ -260,7 +260,9 @@ public class ScriptTemplateView extends AbstractUrlBasedView { setEngine(createEngineFromName()); } - Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); + if (this.renderFunction != null && this.engine != null) { + Assert.isInstanceOf(Invocable.class, this.engine, "ScriptEngine must implement Invocable when 'renderFunction' is specified."); + } } protected ScriptEngine getEngine() { @@ -357,7 +359,6 @@ public class ScriptTemplateView extends AbstractUrlBasedView { try { ScriptEngine engine = getEngine(); - Invocable invocable = (Invocable) engine; String url = getUrl(); Assert.state(url != null, "'url' not set"); String template = getTemplate(url); @@ -372,12 +373,18 @@ public class ScriptTemplateView extends AbstractUrlBasedView { RenderingContext context = new RenderingContext(obtainApplicationContext(), this.locale, templateLoader, url); Object html; - if (this.renderObject != null) { + if (this.renderFunction == null) { + SimpleBindings bindings = new SimpleBindings(); + bindings.putAll(model); + model.put("renderingContext", context); + html = engine.eval(template, bindings); + } + else if (this.renderObject != null) { Object thiz = engine.eval(this.renderObject); - html = invocable.invokeMethod(thiz, this.renderFunction, template, model, context); + html = ((Invocable)engine).invokeMethod(thiz, this.renderFunction, template, model, context); } else { - html = invocable.invokeFunction(this.renderFunction, template, model, context); + html = ((Invocable)engine).invokeFunction(this.renderFunction, template, model, context); } response.getWriter().write(String.valueOf(html)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java index ba0311aeeb6..6ebaa384057 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java @@ -74,6 +74,19 @@ public class KotlinScriptTemplateTests { response.getContentAsString()); } + @Test + public void renderTemplateWithoutRenderFunction() throws Exception { + Map model = new HashMap<>(); + model.put("header", ""); + model.put("hello", "Hello"); + model.put("foo", "Foo"); + model.put("footer", ""); + MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/kotlin/eval.kts", + model, Locale.ENGLISH, ScriptTemplatingConfigurationWithoutRenderFunction.class); + assertEquals("\n

Hello Foo

\n", + response.getContentAsString()); + } + private MockHttpServletResponse renderViewWithModel(String viewUrl, Map model, Locale locale, Class configuration) throws Exception { ScriptTemplateView view = createViewWithUrl(viewUrl, configuration); view.setLocale(locale); @@ -116,4 +129,13 @@ public class KotlinScriptTemplateTests { } } + @Configuration + static class ScriptTemplatingConfigurationWithoutRenderFunction { + + @Bean + public ScriptTemplateConfigurer kotlinScriptConfigurer() { + return new ScriptTemplateConfigurer("kotlin"); + } + } + } 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 index 034308970f3..c6f3daa961e 100644 --- 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 @@ -166,17 +166,16 @@ public class ScriptTemplateViewTests { @Test public void nonInvocableScriptEngine() throws Exception { - this.expectedException.expect(IllegalArgumentException.class); this.view.setEngine(mock(ScriptEngine.class)); - this.expectedException.expectMessage(contains("instance")); + this.view.setApplicationContext(this.wac); } @Test - public void noRenderFunctionDefined() { - this.view.setEngine(mock(InvocableScriptEngine.class)); + public void nonInvocableScriptEngineWithRenderFunction() throws Exception { + this.view.setEngine(mock(ScriptEngine.class)); + this.view.setRenderFunction("render"); this.expectedException.expect(IllegalArgumentException.class); this.view.setApplicationContext(this.wac); - this.expectedException.expectMessage(contains("renderFunction")); } @Test diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts new file mode 100644 index 00000000000..6a8121acf02 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts @@ -0,0 +1,4 @@ +// TODO Improve syntax when KT-15125 will be fixed +"""${bindings["header"]} +

${bindings["hello"]} ${bindings["foo"]}

+${bindings["footer"]}"""