From 2d95199466c52e26ff4c5c3e327e8ae6114e807f Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 23 Jan 2017 18:37:27 +0100 Subject: [PATCH] Support i18n and nested templates in ScriptTemplateView This commit changes the 3rd parameter passed to the rendering function from String url to RenderingContext renderingContext. RenderingContext contains 4 properties: - ApplicationContext applicationContext - Locale locale - Function templateLoader - String url Issue: SPR-15064 --- build.gradle | 2 + .../result/view/script/RenderingContext.java | 66 +++++++++++++++++++ .../view/script/ScriptTemplateView.java | 23 ++++++- .../script/ScriptTemplateViewResolver.java | 14 ++++ .../script/KotlinScriptTemplateTests.java | 31 +++++++-- .../ScriptTemplateWithBindingsExtensions.kt | 14 ++++ .../result/view/script/jruby/render.rb | 2 +- .../result/view/script/jython/render.py | 2 +- .../result/view/script/kotlin/footer.html | 1 + .../result/view/script/kotlin/header.html | 1 + .../result/view/script/kotlin/render.kts | 9 ++- .../result/view/script/kotlin/template.kts | 6 +- .../result/view/script/messages_en.properties | 1 + .../result/view/script/messages_fr.properties | 1 + .../result/view/script/nashorn/render.js | 4 +- .../servlet/view/script/RenderingContext.java | 66 +++++++++++++++++++ .../view/script/ScriptTemplateView.java | 24 ++++++- .../script/ScriptTemplateViewResolver.java | 9 +++ .../script/KotlinScriptTemplateTests.java | 31 +++++++-- .../ScriptTemplateWithBindingsExtensions.kt | 14 ++++ .../web/servlet/view/script/jruby/render.rb | 2 +- .../web/servlet/view/script/jython/render.py | 2 +- .../servlet/view/script/kotlin/footer.html | 1 + .../servlet/view/script/kotlin/header.html | 1 + .../web/servlet/view/script/kotlin/render.kts | 9 ++- .../servlet/view/script/kotlin/template.kts | 6 +- .../view/script/messages_en.properties | 1 + .../view/script/messages_fr.properties | 1 + .../web/servlet/view/script/nashorn/render.js | 4 +- 29 files changed, 320 insertions(+), 28 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/RenderingContext.java create mode 100644 spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/result/view/script/ScriptTemplateWithBindingsExtensions.kt create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/footer.html create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/header.html create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_en.properties create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_fr.properties create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/RenderingContext.java create mode 100644 spring-webmvc/src/test/kotlin/org/springframework/web/servlet/view/script/ScriptTemplateWithBindingsExtensions.kt create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/footer.html create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/header.html create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_en.properties create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_fr.properties diff --git a/build.gradle b/build.gradle index 2675a80008..864f84f1cc 100644 --- a/build.gradle +++ b/build.gradle @@ -870,6 +870,7 @@ project("spring-web-reactive") { // the case yet, so we depend on kotlin-script-util and exclude these // dependencies only used for artifact retrieval. Point raised to Kotlin team. testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}") + testCompile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") { exclude group: "com.jcabi", module: "jcabi-aether" exclude group: "org.apache.maven", module: "maven-core" @@ -983,6 +984,7 @@ project("spring-webmvc") { // the case yet, so we depend on kotlin-script-util and exclude these // dependencies only used for artifact retrieval. Point raised to Kotlin team. testRuntime("org.jetbrains.kotlin:kotlin-compiler:${kotlinVersion}") + testCompile("org.jetbrains.kotlin:kotlin-script-runtime:${kotlinVersion}") testRuntime("org.jetbrains.kotlin:kotlin-script-util:${kotlinVersion}") { exclude group: "com.jcabi", module: "jcabi-aether" exclude group: "org.apache.maven", module: "maven-core" diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/RenderingContext.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/RenderingContext.java new file mode 100644 index 0000000000..a0c1ffe7b5 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/script/RenderingContext.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 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.Locale; +import java.util.function.Function; + +import org.springframework.context.ApplicationContext; + +/** + * Context passed to {@link ScriptTemplateView} render function. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class RenderingContext { + + private final ApplicationContext applicationContext; + + private final Locale locale; + + private final Function templateLoader; + + private final String url; + + + public RenderingContext(ApplicationContext applicationContext, Locale locale, + Function templateLoader, String url) { + this.applicationContext = applicationContext; + this.locale = locale; + this.templateLoader = templateLoader; + this.url = url; + } + + + public ApplicationContext getApplicationContext() { + return applicationContext; + } + + public Locale getLocale() { + return locale; + } + + public Function getTemplateLoader() { + return templateLoader; + } + + public String getUrl() { + return url; + } + +} 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 index ebf11348aa..b50c708288 100644 --- 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 @@ -21,6 +21,7 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; @@ -74,6 +75,8 @@ public class ScriptTemplateView extends AbstractUrlBasedView { private String engineName; + private Locale locale; + private Boolean sharedEngine; private String[] scripts; @@ -119,6 +122,13 @@ public class ScriptTemplateView extends AbstractUrlBasedView { this.engineName = engineName; } + /** + * Set the {@link Locale} to pass to the render function. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + /** * See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation. */ @@ -284,14 +294,23 @@ public class ScriptTemplateView extends AbstractUrlBasedView { Invocable invocable = (Invocable) engine; String url = getUrl(); String template = getTemplate(url); + Function templateLoader = path -> { + try { + return getTemplate(path); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + }; + RenderingContext context = new RenderingContext(this.getApplicationContext(), this.locale, templateLoader, url); Object html; if (this.renderObject != null) { Object thiz = engine.eval(this.renderObject); - html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url); + html = invocable.invokeMethod(thiz, this.renderFunction, template, model, context); } else { - html = invocable.invokeFunction(this.renderFunction, template, model, url); + html = invocable.invokeFunction(this.renderFunction, template, model, context); } byte[] bytes = String.valueOf(html).getBytes(StandardCharsets.UTF_8); 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 index d24643929f..df34ef2e81 100644 --- 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 @@ -16,7 +16,13 @@ package org.springframework.web.reactive.result.view.script; +import java.util.Locale; + +import reactor.core.publisher.Mono; + +import org.springframework.web.reactive.result.view.AbstractUrlBasedView; import org.springframework.web.reactive.result.view.UrlBasedViewResolver; +import org.springframework.web.reactive.result.view.View; /** * Convenience subclass of {@link UrlBasedViewResolver} that supports @@ -55,6 +61,14 @@ public class ScriptTemplateViewResolver extends UrlBasedViewResolver { setSuffix(suffix); } + @Override + public Mono resolveViewName(String viewName, Locale locale) { + return super.resolveViewName(viewName, locale).map(view -> { + ((ScriptTemplateView)view).setLocale(locale); + return view; + }); + } + @Override protected Class requiredViewClass() { return ScriptTemplateView.class; 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 index 936e8cb826..b35522a099 100644 --- 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 @@ -17,6 +17,7 @@ package org.springframework.web.reactive.result.view.script; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -26,6 +27,7 @@ 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.ResourceBundleMessageSource; import org.springframework.context.support.StaticApplicationContext; import org.springframework.http.MediaType; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; @@ -50,18 +52,28 @@ public class KotlinScriptTemplateTests { } @Test - public void renderTemplate() throws Exception { + public void renderTemplateWithFrenchLocale() throws Exception { Map model = new HashMap<>(); - model.put("title", "Layout example"); - model.put("body", "This is the body"); + model.put("foo", "Foo"); MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/template.kts", - model, ScriptTemplatingConfiguration.class); - assertEquals("Layout example

This is the body

", + model, Locale.FRENCH, ScriptTemplatingConfiguration.class); + assertEquals("\n

Bonjour Foo

\n", response.getBodyAsString().block()); } - private MockServerHttpResponse renderViewWithModel(String viewUrl, Map model, Class configuration) throws Exception { + @Test + public void renderTemplateWithEnglishLocale() throws Exception { + Map model = new HashMap<>(); + model.put("foo", "Foo"); + MockServerHttpResponse response = renderViewWithModel("org/springframework/web/reactive/result/view/script/kotlin/template.kts", + model, Locale.ENGLISH, ScriptTemplatingConfiguration.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); MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); MockServerHttpResponse response = new MockServerHttpResponse(); WebSessionManager manager = new DefaultWebSessionManager(); @@ -94,6 +106,13 @@ public class KotlinScriptTemplateTests { configurer.setRenderFunction("render"); return configurer; } + + @Bean + public ResourceBundleMessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("org/springframework/web/reactive/result/view/script/messages"); + return messageSource; + } } } diff --git a/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/result/view/script/ScriptTemplateWithBindingsExtensions.kt b/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/result/view/script/ScriptTemplateWithBindingsExtensions.kt new file mode 100644 index 0000000000..e40f00b18c --- /dev/null +++ b/spring-web-reactive/src/test/kotlin/org/springframework/web/reactive/result/view/script/ScriptTemplateWithBindingsExtensions.kt @@ -0,0 +1,14 @@ +package org.springframework.web.reactive.result.view.script + +import kotlin.script.templates.standard.ScriptTemplateWithBindings + +fun ScriptTemplateWithBindings.include(path: String) = + (bindings["include"] as (String) -> String).invoke(path) + + +fun ScriptTemplateWithBindings.i18n(code: String) = + (bindings["i18n"] as (String) -> String).invoke(code) + +var ScriptTemplateWithBindings.foo: String + get() = bindings["foo"] as String + set(value) { throw UnsupportedOperationException()} 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 index e5db6520d5..f22ea3bd01 100644 --- 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 @@ -3,7 +3,7 @@ require 'ostruct' require 'java' # Renders an ERB template against a hashmap of variables. -def render(template, variables, url) +def render(template, variables, renderingContext) context = OpenStruct.new(variables).instance_eval do variables.each do |k, v| instance_variable_set(k, v) if k[0] == '@' 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 index 07c73ae869..50b5f68d73 100644 --- 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 @@ -1,5 +1,5 @@ from string import Template -def render(template, model, url): +def render(template, model, renderingContext): 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/kotlin/footer.html b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/footer.html new file mode 100644 index 0000000000..8634a86d23 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/header.html b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/header.html new file mode 100644 index 0000000000..7b5b91f9a7 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/kotlin/header.html @@ -0,0 +1 @@ + \ 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 index d1ab1ae6db..2ea857f528 100644 --- 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 @@ -1,10 +1,17 @@ import javax.script.* +import org.springframework.web.reactive.result.view.script.RenderingContext +import org.springframework.context.support.ResourceBundleMessageSource +import org.springframework.beans.factory.getBean // 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 { +fun render(template: String, model: Map, renderingContext: RenderingContext): String { val engine = ScriptEngineManager().getEngineByName("kotlin") val bindings = SimpleBindings() bindings.putAll(model) + var messageSource = renderingContext.applicationContext.getBean() + bindings.put("i18n", { code: String -> messageSource.getMessage(code, null, renderingContext.locale) }) + bindings.put("include", { path: String -> renderingContext.templateLoader.apply("org/springframework/web/reactive/result/view/script/kotlin/$path.html") }) 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 index bacaa3f3d0..5f4a0d5629 100644 --- 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 @@ -1 +1,5 @@ -"""${bindings["title"]}

${bindings["body"]}

""" +import org.springframework.web.reactive.result.view.script.* + +"""${include("header") } +

${i18n("hello")} $foo

+${include("footer")}""" diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_en.properties b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_en.properties new file mode 100644 index 0000000000..46e6ccaf4f --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_en.properties @@ -0,0 +1 @@ +hello = Hello diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_fr.properties b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_fr.properties new file mode 100644 index 0000000000..ff4bea1c64 --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/result/view/script/messages_fr.properties @@ -0,0 +1 @@ +hello = Bonjour 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 index 8b6a712028..f995bd5a9b 100644 --- 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 @@ -2,6 +2,6 @@ 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); +function renderWithUrl(template, model, renderingContext) { + return template.replace("{{title}}", "Check url parameter").replace("{{body}}", renderingContext.url); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/RenderingContext.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/RenderingContext.java new file mode 100644 index 0000000000..26fc1a20ed --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/RenderingContext.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.script; + +import java.util.Locale; +import java.util.function.Function; + +import org.springframework.context.ApplicationContext; + +/** + * Context passed to {@link ScriptTemplateView} render function. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class RenderingContext { + + private final ApplicationContext applicationContext; + + private final Locale locale; + + private final Function templateLoader; + + private final String url; + + + public RenderingContext(ApplicationContext applicationContext, Locale locale, + Function templateLoader, String url) { + this.applicationContext = applicationContext; + this.locale = locale; + this.templateLoader = templateLoader; + this.url = url; + } + + + public ApplicationContext getApplicationContext() { + return applicationContext; + } + + public Locale getLocale() { + return locale; + } + + public Function getTemplateLoader() { + return templateLoader; + } + + public String getUrl() { + return url; + } + +} 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 4ae3b1b76f..fe4a168896 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 @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; @@ -83,6 +84,8 @@ public class ScriptTemplateView extends AbstractUrlBasedView { private String engineName; + private Locale locale; + private Boolean sharedEngine; private String[] scripts; @@ -126,6 +129,14 @@ public class ScriptTemplateView extends AbstractUrlBasedView { this.engine = engine; } + /** + * Set the {@link Locale} to pass to the render function. + * @since 5.0 + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + /** * See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation. */ @@ -345,14 +356,23 @@ public class ScriptTemplateView extends AbstractUrlBasedView { Invocable invocable = (Invocable) engine; String url = getUrl(); String template = getTemplate(url); + Function templateLoader = path -> { + try { + return getTemplate(path); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + }; + RenderingContext context = new RenderingContext(this.getApplicationContext(), this.locale, templateLoader, url); Object html; if (this.renderObject != null) { Object thiz = engine.eval(this.renderObject); - html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url); + html = invocable.invokeMethod(thiz, this.renderFunction, template, model, context); } else { - html = invocable.invokeFunction(this.renderFunction, template, model, url); + html = invocable.invokeFunction(this.renderFunction, template, model, context); } response.getWriter().write(String.valueOf(html)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java index 168ce304c2..bbea2451db 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java @@ -16,6 +16,9 @@ package org.springframework.web.servlet.view.script; +import java.util.Locale; + +import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.UrlBasedViewResolver; /** @@ -56,6 +59,12 @@ public class ScriptTemplateViewResolver extends UrlBasedViewResolver { setSuffix(suffix); } + @Override + protected View createView(String viewName, Locale locale) throws Exception { + ScriptTemplateView view = (ScriptTemplateView)super.createView(viewName, locale); + view.setLocale(locale); + return view; + } @Override protected Class requiredViewClass() { 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 362b78b31d..ba0311aeeb 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 @@ -17,6 +17,7 @@ package org.springframework.web.servlet.view.script; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.servlet.ServletContext; @@ -28,6 +29,7 @@ import static org.mockito.Mockito.mock; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.mock.web.test.MockServletContext; @@ -53,18 +55,28 @@ public class KotlinScriptTemplateTests { } @Test - public void renderTemplate() throws Exception { + public void renderTemplateWithFrenchLocale() throws Exception { Map model = new HashMap<>(); - model.put("title", "Layout example"); - model.put("body", "This is the body"); + model.put("foo", "Foo"); MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/kotlin/template.kts", - model, ScriptTemplatingConfiguration.class); - assertEquals("Layout example

This is the body

", + model, Locale.FRENCH, ScriptTemplatingConfiguration.class); + assertEquals("\n

Bonjour Foo

\n", response.getContentAsString()); } - private MockHttpServletResponse renderViewWithModel(String viewUrl, Map model, Class configuration) throws Exception { + @Test + public void renderTemplateWithEnglishLocale() throws Exception { + Map model = new HashMap<>(); + model.put("foo", "Foo"); + MockHttpServletResponse response = renderViewWithModel("org/springframework/web/servlet/view/script/kotlin/template.kts", + model, Locale.ENGLISH, ScriptTemplatingConfiguration.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); MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletRequest request = new MockHttpServletRequest(); view.renderMergedOutputModel(model, request, response); @@ -95,6 +107,13 @@ public class KotlinScriptTemplateTests { configurer.setRenderFunction("render"); return configurer; } + + @Bean + public ResourceBundleMessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + messageSource.setBasename("org/springframework/web/servlet/view/script/messages"); + return messageSource; + } } } diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/view/script/ScriptTemplateWithBindingsExtensions.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/view/script/ScriptTemplateWithBindingsExtensions.kt new file mode 100644 index 0000000000..e40f00b18c --- /dev/null +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/view/script/ScriptTemplateWithBindingsExtensions.kt @@ -0,0 +1,14 @@ +package org.springframework.web.reactive.result.view.script + +import kotlin.script.templates.standard.ScriptTemplateWithBindings + +fun ScriptTemplateWithBindings.include(path: String) = + (bindings["include"] as (String) -> String).invoke(path) + + +fun ScriptTemplateWithBindings.i18n(code: String) = + (bindings["i18n"] as (String) -> String).invoke(code) + +var ScriptTemplateWithBindings.foo: String + get() = bindings["foo"] as String + set(value) { throw UnsupportedOperationException()} diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jruby/render.rb b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jruby/render.rb index e5db6520d5..f22ea3bd01 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jruby/render.rb +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jruby/render.rb @@ -3,7 +3,7 @@ require 'ostruct' require 'java' # Renders an ERB template against a hashmap of variables. -def render(template, variables, url) +def render(template, variables, renderingContext) context = OpenStruct.new(variables).instance_eval do variables.each do |k, v| instance_variable_set(k, v) if k[0] == '@' diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jython/render.py b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jython/render.py index 07c73ae869..50b5f68d73 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jython/render.py +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/jython/render.py @@ -1,5 +1,5 @@ from string import Template -def render(template, model, url): +def render(template, model, renderingContext): s = Template(template) return s.substitute(model) \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/footer.html b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/footer.html new file mode 100644 index 0000000000..8634a86d23 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/header.html b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/header.html new file mode 100644 index 0000000000..7b5b91f9a7 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/header.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/render.kts b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/render.kts index d1ab1ae6db..6c6f480200 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/render.kts +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/render.kts @@ -1,10 +1,17 @@ import javax.script.* +import org.springframework.web.servlet.view.script.RenderingContext +import org.springframework.context.support.ResourceBundleMessageSource +import org.springframework.beans.factory.getBean // 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 { +fun render(template: String, model: Map, renderingContext: RenderingContext): String { val engine = ScriptEngineManager().getEngineByName("kotlin") val bindings = SimpleBindings() bindings.putAll(model) + var messageSource = renderingContext.applicationContext.getBean() + bindings.put("i18n", { code: String -> messageSource.getMessage(code, null, renderingContext.locale) }) + bindings.put("include", { path: String -> renderingContext.templateLoader.apply("org/springframework/web/servlet/view/script/kotlin/$path.html") }) engine.setBindings(bindings, ScriptContext.ENGINE_SCOPE) return engine.eval(template) as String } + diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/template.kts b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/template.kts index bacaa3f3d0..5f4a0d5629 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/template.kts +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/kotlin/template.kts @@ -1 +1,5 @@ -"""${bindings["title"]}

${bindings["body"]}

""" +import org.springframework.web.reactive.result.view.script.* + +"""${include("header") } +

${i18n("hello")} $foo

+${include("footer")}""" diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_en.properties b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_en.properties new file mode 100644 index 0000000000..46e6ccaf4f --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_en.properties @@ -0,0 +1 @@ +hello = Hello diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_fr.properties b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_fr.properties new file mode 100644 index 0000000000..ff4bea1c64 --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/messages_fr.properties @@ -0,0 +1 @@ +hello = Bonjour diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/nashorn/render.js b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/nashorn/render.js index 8b6a712028..f995bd5a9b 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/nashorn/render.js +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script/nashorn/render.js @@ -2,6 +2,6 @@ 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); +function renderWithUrl(template, model, renderingContext) { + return template.replace("{{title}}", "Check url parameter").replace("{{body}}", renderingContext.url); }