From 8b95697c8d119d0d2b2915c683ca8169546c6234 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:03:26 +0200 Subject: [PATCH] Set output_encoding in FreeMarkerView implementations According to the official FreeMarker documentation, Spring's FreeMarkerView implementations should be configuring the output_encoding for template rendering. To address that, this commit modifies the FreeMarkerView implementations in Web MVC and WebFlux to explicitly set the output_encoding for template rendering. See https://freemarker.apache.org/docs/pgui_misc_charset.html#autoid_53 See gh-33071 Closes gh-33106 --- .../view/freemarker/FreeMarkerView.java | 5 ++- ...WebFluxViewResolutionIntegrationTests.java | 16 +++++-- .../web/reactive/config/index_ISO-8859-1.ftl | 7 +++- .../web/reactive/config/index_UTF-8.ftl | 7 +++- .../view/freemarker/FreeMarkerView.java | 14 +++++-- .../ViewResolutionIntegrationTests.java | 42 ++++++++++++------- .../config/annotation/WEB-INF/index.ftl | 7 +++- 7 files changed, 71 insertions(+), 27 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java index 15253c7fa5c..89386db8958 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java @@ -26,6 +26,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import freemarker.core.Environment; import freemarker.core.ParseException; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapperBuilder; @@ -333,7 +334,9 @@ public class FreeMarkerView extends AbstractUrlBasedView { FastByteArrayOutputStream bos = new FastByteArrayOutputStream(); Charset charset = getCharset(contentType); Writer writer = new OutputStreamWriter(bos, charset); - template.process(freeMarkerModel, writer); + Environment env = template.createProcessingEnvironment(freeMarkerModel, writer); + env.setOutputEncoding(charset.name()); + env.process(); byte[] bytes = bos.toByteArrayUnsafe(); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return Mono.just(buffer); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java index e29dde154f3..e13bb154172 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java @@ -58,12 +58,20 @@ class WebFluxViewResolutionIntegrationTests { private static final MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1"); - private static final String EXPECTED_BODY = "
Hello, Java Café"; @Nested class FreeMarkerTests { + private static final String EXPECTED_BODY = """ + + +output_encoding: %s
+ + + """; + private static final ClassTemplateLoader classTemplateLoader = new ClassTemplateLoader(WebFluxViewResolutionIntegrationTests.class, ""); @@ -77,21 +85,21 @@ class WebFluxViewResolutionIntegrationTests { @Test void freemarkerWithDefaults() throws Exception { MockServerHttpResponse response = runTest(FreeMarkerWebFluxConfig.class); - StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY.formatted("UTF-8")).expectComplete().verify(); assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8); } @Test void freemarkerWithExplicitDefaultEncoding() throws Exception { MockServerHttpResponse response = runTest(ExplicitDefaultEncodingConfig.class); - StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY.formatted("UTF-8")).expectComplete().verify(); assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8); } @Test void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception { MockServerHttpResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class); - StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).expectComplete().verify(); + StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY.formatted("ISO-8859-1")).expectComplete().verify(); // When the Content-Type (supported media type) is explicitly set on the view resolver, it should be used. assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_ISO_8859_1); } diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl b/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl index ba27073c5c5..44734f86718 100644 --- a/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl @@ -1 +1,6 @@ -${hello}, Java Café \ No newline at end of file + + +output_encoding: ${.output_encoding}
+ + diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl b/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl index 84bf3b81e10..e7805fccd54 100644 --- a/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl @@ -1 +1,6 @@ -${hello}, Java Café \ No newline at end of file + + +output_encoding: ${.output_encoding}
+ + diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java index c9f829172bc..533a398be21 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java @@ -22,6 +22,7 @@ import java.nio.charset.Charset; import java.util.Locale; import java.util.Map; +import freemarker.core.Environment; import freemarker.core.ParseException; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapperBuilder; @@ -364,19 +365,26 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Process the FreeMarker template to the servlet response. + * Process the FreeMarker template and write the result to the response. + *As of Spring Framework 6.2, this method sets the + * {@linkplain Environment#setOutputEncoding(String) output encoding} of the + * FreeMarker {@link Environment} to the character encoding of the supplied + * {@link HttpServletResponse}. *
Can be overridden to customize the behavior. * @param template the template to process * @param model the model for the template * @param response servlet response (use this to get the OutputStream or Writer) * @throws IOException if the template file could not be retrieved * @throws TemplateException if thrown by FreeMarker - * @see freemarker.template.Template#process(Object, java.io.Writer) + * @see freemarker.template.Template#createProcessingEnvironment(Object, java.io.Writer) + * @see freemarker.core.Environment#process() */ protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException { - template.process(model, response.getWriter()); + Environment env = template.createProcessingEnvironment(model, response.getWriter()); + env.setOutputEncoding(response.getCharacterEncoding()); + env.process(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java index 6b7654179be..7c23ec140c0 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java @@ -48,9 +48,6 @@ import static org.assertj.core.api.Assertions.assertThatRuntimeException; */ class ViewResolutionIntegrationTests { - private static final String EXPECTED_BODY = "
Hello, Java Café"; - - @BeforeAll static void verifyDefaultFileEncoding() { assertThat(System.getProperty("file.encoding")).as("JVM default file encoding").isEqualTo("UTF-8"); @@ -60,6 +57,15 @@ class ViewResolutionIntegrationTests { @Nested class FreeMarkerTests { + private static final String EXPECTED_BODY = """ + + +output_encoding: %s
+ + + """; + @Test void freemarkerWithInvalidConfig() { assertThatRuntimeException() @@ -69,45 +75,49 @@ class ViewResolutionIntegrationTests { @Test void freemarkerWithDefaults() throws Exception { + String encoding = "ISO-8859-1"; MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class); assertThat(response.isCharset()).as("character encoding set in response").isTrue(); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding)); // Prior to Spring Framework 6.2, the charset is not updated in the Content-Type. // Thus, we expect ISO-8859-1 instead of UTF-8. - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); + assertThat(response.getCharacterEncoding()).isEqualTo(encoding); + assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding); } @Test // gh-16629, gh-33071 void freemarkerWithExistingViewResolver() throws Exception { + String encoding = "ISO-8859-1"; MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class); assertThat(response.isCharset()).as("character encoding set in response").isTrue(); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding)); // Prior to Spring Framework 6.2, the charset is not updated in the Content-Type. // Thus, we expect ISO-8859-1 instead of UTF-8. - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); + assertThat(response.getCharacterEncoding()).isEqualTo(encoding); + assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding); } @Test // gh-33071 void freemarkerWithExplicitDefaultEncoding() throws Exception { + String encoding = "ISO-8859-1"; MockHttpServletResponse response = runTest(ExplicitDefaultEncodingConfig.class); assertThat(response.isCharset()).as("character encoding set in response").isTrue(); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding)); // Prior to Spring Framework 6.2, the charset is not updated in the Content-Type. // Thus, we expect ISO-8859-1 instead of UTF-8. - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); + assertThat(response.getCharacterEncoding()).isEqualTo(encoding); + assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding); } @Test // gh-33071 void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception { + String encoding = "UTF-16"; MockHttpServletResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class); assertThat(response.isCharset()).as("character encoding set in response").isTrue(); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding)); // When the Content-Type is explicitly set on the view resolver, it should be used. - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-16"); - assertThat(response.getContentType()).isEqualTo("text/html;charset=UTF-16"); + assertThat(response.getCharacterEncoding()).isEqualTo(encoding); + assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding); } @@ -202,7 +212,7 @@ class ViewResolutionIntegrationTests { @Test void groovyMarkup() throws Exception { MockHttpServletResponse response = runTest(GroovyMarkupWebConfig.class); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + assertThat(response.getContentAsString()).isEqualTo("Hello, Java Café"); } diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl index 84bf3b81e10..e7805fccd54 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl @@ -1 +1,6 @@ -${hello}, Java Café \ No newline at end of file + + +output_encoding: ${.output_encoding}
+ +