Browse Source

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
pull/33111/head
Sam Brannen 2 years ago
parent
commit
8b95697c8d
  1. 5
      spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java
  2. 16
      spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java
  3. 7
      spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl
  4. 7
      spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl
  5. 14
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java
  6. 42
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java
  7. 7
      spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl

5
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.Map;
import java.util.Optional; import java.util.Optional;
import freemarker.core.Environment;
import freemarker.core.ParseException; import freemarker.core.ParseException;
import freemarker.template.Configuration; import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder; import freemarker.template.DefaultObjectWrapperBuilder;
@ -333,7 +334,9 @@ public class FreeMarkerView extends AbstractUrlBasedView {
FastByteArrayOutputStream bos = new FastByteArrayOutputStream(); FastByteArrayOutputStream bos = new FastByteArrayOutputStream();
Charset charset = getCharset(contentType); Charset charset = getCharset(contentType);
Writer writer = new OutputStreamWriter(bos, charset); 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(); byte[] bytes = bos.toByteArrayUnsafe();
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return Mono.just(buffer); return Mono.just(buffer);

16
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 MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1");
private static final String EXPECTED_BODY = "<html><body>Hello, Java Café</body></html>";
@Nested @Nested
class FreeMarkerTests { class FreeMarkerTests {
private static final String EXPECTED_BODY = """
<html>
<body>
<h1>Hello, Java Café</h1>
<p>output_encoding: %s</p>
</body>
</html>
""";
private static final ClassTemplateLoader classTemplateLoader = private static final ClassTemplateLoader classTemplateLoader =
new ClassTemplateLoader(WebFluxViewResolutionIntegrationTests.class, ""); new ClassTemplateLoader(WebFluxViewResolutionIntegrationTests.class, "");
@ -77,21 +85,21 @@ class WebFluxViewResolutionIntegrationTests {
@Test @Test
void freemarkerWithDefaults() throws Exception { void freemarkerWithDefaults() throws Exception {
MockServerHttpResponse response = runTest(FreeMarkerWebFluxConfig.class); 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); assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8);
} }
@Test @Test
void freemarkerWithExplicitDefaultEncoding() throws Exception { void freemarkerWithExplicitDefaultEncoding() throws Exception {
MockServerHttpResponse response = runTest(ExplicitDefaultEncodingConfig.class); 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); assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_UTF8);
} }
@Test @Test
void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception { void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception {
MockServerHttpResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class); 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. // 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); assertThat(response.getHeaders().getContentType()).isEqualTo(TEXT_HTML_ISO_8859_1);
} }

7
spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl

@ -1 +1,6 @@
<html><body>${hello}, Java Café</body></html> <html>
<body>
<h1>${hello}, Java Café</h1>
<p>output_encoding: ${.output_encoding}</p>
</body>
</html>

7
spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl

@ -1 +1,6 @@
<html><body>${hello}, Java Café</body></html> <html>
<body>
<h1>${hello}, Java Café</h1>
<p>output_encoding: ${.output_encoding}</p>
</body>
</html>

14
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.Locale;
import java.util.Map; import java.util.Map;
import freemarker.core.Environment;
import freemarker.core.ParseException; import freemarker.core.ParseException;
import freemarker.template.Configuration; import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapperBuilder; 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.
* <p>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}.
* <p>Can be overridden to customize the behavior. * <p>Can be overridden to customize the behavior.
* @param template the template to process * @param template the template to process
* @param model the model for the template * @param model the model for the template
* @param response servlet response (use this to get the OutputStream or Writer) * @param response servlet response (use this to get the OutputStream or Writer)
* @throws IOException if the template file could not be retrieved * @throws IOException if the template file could not be retrieved
* @throws TemplateException if thrown by FreeMarker * @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) protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response)
throws IOException, TemplateException { throws IOException, TemplateException {
template.process(model, response.getWriter()); Environment env = template.createProcessingEnvironment(model, response.getWriter());
env.setOutputEncoding(response.getCharacterEncoding());
env.process();
} }

42
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 { class ViewResolutionIntegrationTests {
private static final String EXPECTED_BODY = "<html><body>Hello, Java Café</body></html>";
@BeforeAll @BeforeAll
static void verifyDefaultFileEncoding() { static void verifyDefaultFileEncoding() {
assertThat(System.getProperty("file.encoding")).as("JVM default file encoding").isEqualTo("UTF-8"); assertThat(System.getProperty("file.encoding")).as("JVM default file encoding").isEqualTo("UTF-8");
@ -60,6 +57,15 @@ class ViewResolutionIntegrationTests {
@Nested @Nested
class FreeMarkerTests { class FreeMarkerTests {
private static final String EXPECTED_BODY = """
<html>
<body>
<h1>Hello, Java Café</h1>
<p>output_encoding: %s</p>
</body>
</html>
""";
@Test @Test
void freemarkerWithInvalidConfig() { void freemarkerWithInvalidConfig() {
assertThatRuntimeException() assertThatRuntimeException()
@ -69,45 +75,49 @@ class ViewResolutionIntegrationTests {
@Test @Test
void freemarkerWithDefaults() throws Exception { void freemarkerWithDefaults() throws Exception {
String encoding = "ISO-8859-1";
MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class); MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class);
assertThat(response.isCharset()).as("character encoding set in response").isTrue(); 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. // 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. // Thus, we expect ISO-8859-1 instead of UTF-8.
assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
} }
@Test // gh-16629, gh-33071 @Test // gh-16629, gh-33071
void freemarkerWithExistingViewResolver() throws Exception { void freemarkerWithExistingViewResolver() throws Exception {
String encoding = "ISO-8859-1";
MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class); MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class);
assertThat(response.isCharset()).as("character encoding set in response").isTrue(); 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. // 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. // Thus, we expect ISO-8859-1 instead of UTF-8.
assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
} }
@Test // gh-33071 @Test // gh-33071
void freemarkerWithExplicitDefaultEncoding() throws Exception { void freemarkerWithExplicitDefaultEncoding() throws Exception {
String encoding = "ISO-8859-1";
MockHttpServletResponse response = runTest(ExplicitDefaultEncodingConfig.class); MockHttpServletResponse response = runTest(ExplicitDefaultEncodingConfig.class);
assertThat(response.isCharset()).as("character encoding set in response").isTrue(); 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. // 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. // Thus, we expect ISO-8859-1 instead of UTF-8.
assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
assertThat(response.getContentType()).isEqualTo("text/html;charset=ISO-8859-1"); assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
} }
@Test // gh-33071 @Test // gh-33071
void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception { void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception {
String encoding = "UTF-16";
MockHttpServletResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class); MockHttpServletResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class);
assertThat(response.isCharset()).as("character encoding set in response").isTrue(); 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. // When the Content-Type is explicitly set on the view resolver, it should be used.
assertThat(response.getCharacterEncoding()).isEqualTo("UTF-16"); assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
assertThat(response.getContentType()).isEqualTo("text/html;charset=UTF-16"); assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
} }
@ -202,7 +212,7 @@ class ViewResolutionIntegrationTests {
@Test @Test
void groovyMarkup() throws Exception { void groovyMarkup() throws Exception {
MockHttpServletResponse response = runTest(GroovyMarkupWebConfig.class); MockHttpServletResponse response = runTest(GroovyMarkupWebConfig.class);
assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); assertThat(response.getContentAsString()).isEqualTo("<html><body>Hello, Java Café</body></html>");
} }

7
spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.ftl

@ -1 +1,6 @@
<html><body>${hello}, Java Café</body></html> <html>
<body>
<h1>${hello}, Java Café</h1>
<p>output_encoding: ${.output_encoding}</p>
</body>
</html>

Loading…
Cancel
Save