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
This commit is contained in:
parent
7e251274ee
commit
d5f9ad03a7
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* <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 {
|
|||
|
||||
/**
|
||||
* 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)}.
|
||||
* <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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<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 {
|
|||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ScriptTemplatingConfigurationWithoutRenderFunction {
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer kotlinScriptConfigurer() {
|
||||
return new ScriptTemplateConfigurer("kotlin");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// TODO Improve syntax when KT-15125 will be fixed
|
||||
"""${bindings["header"]}
|
||||
<p>${bindings["hello"]} ${bindings["foo"]}</p>
|
||||
${bindings["footer"]}"""
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* <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 {
|
|||
|
||||
/**
|
||||
* 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)}.
|
||||
* <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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ScriptTemplatingConfigurationWithoutRenderFunction {
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer kotlinScriptConfigurer() {
|
||||
return new ScriptTemplateConfigurer("kotlin");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// TODO Improve syntax when KT-15125 will be fixed
|
||||
"""${bindings["header"]}
|
||||
<p>${bindings["hello"]} ${bindings["foo"]}</p>
|
||||
${bindings["footer"]}"""
|
||||
Loading…
Reference in New Issue