Browse Source

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
pull/1461/head
Sebastien Deleuze 8 years ago
parent
commit
d5f9ad03a7
  1. 5
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java
  2. 27
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java
  3. 20
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java
  4. 23
      spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java
  5. 9
      spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java
  6. 4
      spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts
  7. 5
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java
  8. 26
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java
  9. 19
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java
  10. 22
      spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java
  11. 9
      spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java
  12. 4
      spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts

5
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfig.java

@ -17,6 +17,8 @@ @@ -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 { @@ -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();

27
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateConfigurer.java

@ -17,6 +17,8 @@ @@ -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 { @@ -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.
* <p>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 { @@ -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 { @@ -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)}.
* <p>This function will be called with the following parameters:
* <ol>
* <li>{@code String template}: the template content</li>
* <li>{@code Map model}: the view model</li>
* <li>{@code String url}: the template url</li>
* <li>{@code RenderingContext context}: the rendering context (since 5.0)</li>
* </ol>
* @see RenderingContext
*/
public void setRenderFunction(String renderFunction) {
this.renderFunction = renderFunction;

20
spring-webflux/src/main/java/org/springframework/web/reactive/result/view/script/ScriptTemplateView.java

@ -26,6 +26,7 @@ import javax.script.Invocable; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);

23
spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/KotlinScriptTemplateTests.java

@ -30,6 +30,7 @@ import org.springframework.http.MediaType; @@ -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 { @@ -60,6 +61,19 @@ public class KotlinScriptTemplateTests {
response.getBodyAsString().block());
}
@Test
public void renderTemplateWithoutRenderFunction() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("header", "<html><body>");
model.put("hello", "Hello");
model.put("foo", "Foo");
model.put("footer", "</body></html>");
MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/eval.kts",
model, Locale.ENGLISH, ScriptTemplatingConfigurationWithoutRenderFunction.class);
assertEquals("<html><body>\n<p>Hello Foo</p>\n</body></html>",
response.getBodyAsString().block());
}
private MockServerHttpResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Locale locale, Class<?> configuration) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl, configuration);
view.setLocale(locale);
@ -101,4 +115,13 @@ public class KotlinScriptTemplateTests { @@ -101,4 +115,13 @@ public class KotlinScriptTemplateTests {
}
}
@Configuration
static class ScriptTemplatingConfigurationWithoutRenderFunction {
@Bean
public ScriptTemplateConfigurer kotlinScriptConfigurer() {
return new ScriptTemplateConfigurer("kotlin");
}
}
}

9
spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script/ScriptTemplateViewTests.java

@ -151,17 +151,16 @@ public class ScriptTemplateViewTests { @@ -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

4
spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/eval.kts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
// TODO Improve syntax when KT-15125 will be fixed
"""${bindings["header"]}
<p>${bindings["hello"]} ${bindings["foo"]}</p>
${bindings["footer"]}"""

5
spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java

@ -17,6 +17,8 @@ @@ -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 { @@ -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();

26
spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java

@ -17,6 +17,8 @@ @@ -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 { @@ -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.
* <p>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 { @@ -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 { @@ -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)}.
* <p>This function will be called with the following parameters:
* <ol>
* <li>{@code String template}: the template content</li>
* <li>{@code Map model}: the view model</li>
* <li>{@code String url}: the template url (since 4.2.2)</li>
* <li>{@code RenderingContext context}: the rendering context (since 5.0)</li>
* </ol>
* @see RenderingContext
*/
public void setRenderFunction(String renderFunction) {
this.renderFunction = renderFunction;

19
spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java

@ -29,6 +29,7 @@ import javax.script.Invocable; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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));

22
spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/KotlinScriptTemplateTests.java

@ -74,6 +74,19 @@ public class KotlinScriptTemplateTests { @@ -74,6 +74,19 @@ public class KotlinScriptTemplateTests {
response.getContentAsString());
}
@Test
public void renderTemplateWithoutRenderFunction() throws Exception {
Map<String, Object> model = new HashMap<>();
model.put("header", "<html><body>");
model.put("hello", "Hello");
model.put("foo", "Foo");
model.put("footer", "</body></html>");
MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/kotlin/eval.kts",
model, Locale.ENGLISH, ScriptTemplatingConfigurationWithoutRenderFunction.class);
assertEquals("<html><body>\n<p>Hello Foo</p>\n</body></html>",
response.getContentAsString());
}
private MockHttpServletResponse renderViewWithModel(String viewUrl, Map<String, Object> model, Locale locale, Class<?> configuration) throws Exception {
ScriptTemplateView view = createViewWithUrl(viewUrl, configuration);
view.setLocale(locale);
@ -116,4 +129,13 @@ public class KotlinScriptTemplateTests { @@ -116,4 +129,13 @@ public class KotlinScriptTemplateTests {
}
}
@Configuration
static class ScriptTemplatingConfigurationWithoutRenderFunction {
@Bean
public ScriptTemplateConfigurer kotlinScriptConfigurer() {
return new ScriptTemplateConfigurer("kotlin");
}
}
}

9
spring-webmvc/src/test/java/org/springframework/web/servlet/view/script/ScriptTemplateViewTests.java

@ -166,17 +166,16 @@ public class ScriptTemplateViewTests { @@ -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

4
spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/eval.kts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
// TODO Improve syntax when KT-15125 will be fixed
"""${bindings["header"]}
<p>${bindings["hello"]} ${bindings["foo"]}</p>
${bindings["footer"]}"""
Loading…
Cancel
Save