Skip to content

Commit b64edb2

Browse files
committed
Update Content-Type based on encoding in MVC FreeMarkerView
Closes gh-33119
1 parent ce53443 commit b64edb2

File tree

12 files changed

+134
-54
lines changed

12 files changed

+134
-54
lines changed

framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-freemarker.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Java::
3636
public FreeMarkerConfigurer freeMarkerConfigurer() {
3737
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
3838
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
39+
configurer.setDefaultCharset(StandardCharsets.UTF_8);
3940
return configurer;
4041
}
4142
}
@@ -58,6 +59,7 @@ Kotlin::
5859
@Bean
5960
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
6061
setTemplateLoaderPath("/WEB-INF/freemarker")
62+
setDefaultCharset(StandardCharsets.UTF_8)
6163
}
6264
}
6365
----
@@ -86,6 +88,7 @@ properties, as the following example shows:
8688
----
8789
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
8890
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
91+
<property name="defaultEncoding" value="UTF-8"/>
8992
</bean>
9093
----
9194

spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
* <p>The simplest way to use this class is to specify a "templateLoaderPath";
6363
* FreeMarker does not need any further configuration then.
6464
*
65-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
65+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
6666
*
6767
* @author Darren Davison
6868
* @author Juergen Hoeller
@@ -143,15 +143,18 @@ public void setFreemarkerVariables(Map<String, Object> variables) {
143143
* files.
144144
* <p>If not specified, FreeMarker will read template files using the platform
145145
* file encoding (defined by the JVM system property {@code file.encoding})
146-
* or {@code "utf-8"} if the platform file encoding is undefined.
147-
* <p>Note that the encoding is not used for template rendering. Instead, an
148-
* explicit encoding must be specified for the rendering process &mdash; for
149-
* example, via Spring's {@code FreeMarkerView} or {@code FreeMarkerViewResolver}.
146+
* or UTF-8 if the platform file encoding is undefined.
147+
* <p>Note that the supplied encoding may or may not be used for template
148+
* rendering. See the documentation for Spring's {@code FreeMarkerView} and
149+
* {@code FreeMarkerViewResolver} implementations for further details.
150150
* @see #setDefaultEncoding(Charset)
151151
* @see freemarker.template.Configuration#setDefaultEncoding
152152
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding
153153
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setContentType
154154
* @see org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver#setContentType
155+
* @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerView#setEncoding
156+
* @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerView#setSupportedMediaTypes
157+
* @see org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver#setSupportedMediaTypes
155158
*/
156159
public void setDefaultEncoding(String defaultEncoding) {
157160
this.defaultEncoding = defaultEncoding;
@@ -170,7 +173,7 @@ public void setDefaultCharset(Charset defaultCharset) {
170173
}
171174

172175
/**
173-
* Set a List of {@link TemplateLoader TemplateLoaders} that will be used to
176+
* Set a list of {@link TemplateLoader TemplateLoaders} that will be used to
174177
* search for templates.
175178
* <p>For example, one or more custom loaders such as database loaders could
176179
* be configured and injected here.
@@ -186,7 +189,7 @@ public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) {
186189
}
187190

188191
/**
189-
* Set a List of {@link TemplateLoader TemplateLoaders} that will be used to
192+
* Set a list of {@link TemplateLoader TemplateLoaders} that will be used to
190193
* search for templates.
191194
* <p>For example, one or more custom loaders such as database loaders could
192195
* be configured and injected here.

spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
* <p>See the {@link FreeMarkerConfigurationFactory} base class for configuration
4646
* details.
4747
*
48-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
48+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
4949
*
5050
* @author Darren Davison
5151
* @since 03.03.2004

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*
2525
* <p>Detected and used by {@link FreeMarkerView}.
2626
*
27+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
28+
*
2729
* @author Rossen Stoyanchev
2830
* @since 5.0
2931
*/

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
* &lt;@spring.bind "person.age"/&gt;
5757
* age is ${spring.status.value}</pre>
5858
*
59-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
59+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
6060
*
6161
* @author Rossen Stoyanchev
6262
* @since 5.0

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerView.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
* sets the supported media type to {@code "text/html;charset=UTF-8"} by default.
9191
* Thus, those default values are likely suitable for most applications.
9292
*
93-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
93+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
9494
*
9595
* @author Rossen Stoyanchev
9696
* @author Sam Brannen
@@ -158,7 +158,7 @@ protected Configuration obtainConfiguration() {
158158
* <p>If the encoding is not explicitly set here or in the FreeMarker
159159
* {@code Configuration}, FreeMarker will read template files using the platform
160160
* file encoding (defined by the JVM system property {@code file.encoding})
161-
* or {@code "utf-8"} if the platform file encoding is undefined. Note,
161+
* or UTF-8 if the platform file encoding is undefined. Note,
162162
* however, that {@link FreeMarkerConfigurer} sets the default encoding in the
163163
* FreeMarker {@code Configuration} to "UTF-8".
164164
* <p>It's recommended to specify the encoding in the FreeMarker {@code Configuration}

spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerViewResolver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,8 @@
2626
* <p>The view class for all views generated by this resolver can be specified
2727
* via the "viewClass" property. See {@link UrlBasedViewResolver} for details.
2828
*
29+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
30+
*
2931
* @author Rossen Stoyanchev
3032
* @since 5.0
3133
*/

spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*
2525
* <p>Detected and used by {@link FreeMarkerView}.
2626
*
27+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
28+
*
2729
* @author Darren Davison
2830
* @author Rob Harrop
2931
* @since 03.03.2004

spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
* &lt;@spring.bind "person.age"/&gt;
6363
* age is ${spring.status.value}</pre>
6464
*
65-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
65+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
6666
*
6767
* @author Darren Davison
6868
* @author Rob Harrop

spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
* byte sequences to character sequences when reading the FreeMarker template file.
5757
* Default is determined by the FreeMarker {@link Configuration}.</li>
5858
* <li><b>{@link #setContentType(String) contentType}</b>: the content type of the
59-
* rendered response. Defaults to {@code "text/html;charset=ISO-8859-1"} but should
60-
* typically be set to a value that corresponds to the actual generated content
59+
* rendered response. Defaults to {@code "text/html;charset=ISO-8859-1"} but may
60+
* need to be set to a value that corresponds to the actual generated content
6161
* type (see note below).</li>
6262
* </ul>
6363
*
@@ -72,9 +72,13 @@
7272
* {@code "text/html;charset=UTF-8"}. When using {@link FreeMarkerViewResolver}
7373
* to create the view for you, set the
7474
* {@linkplain FreeMarkerViewResolver#setContentType(String) content type}
75-
* directly in the {@code FreeMarkerViewResolver}.
75+
* directly in the {@code FreeMarkerViewResolver}; however, as of Spring Framework
76+
* 6.2, it is no longer necessary to explicitly set the content type in the
77+
* {@code FreeMarkerViewResolver} if you have set an explicit encoding via either
78+
* {@link #setEncoding(String)}, {@link FreeMarkerConfigurer#setDefaultEncoding(String)},
79+
* or {@link Configuration#setDefaultEncoding(String)}.
7680
*
77-
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher.
81+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
7882
* As of Spring Framework 6.0, FreeMarker templates are rendered in a minimal
7983
* fashion without JSP support, just exposing request attributes in addition
8084
* to the MVC-provided model map for alignment with common Servlet resources.
@@ -109,13 +113,11 @@ public class FreeMarkerView extends AbstractTemplateView {
109113
* <p>If the encoding is not explicitly set here or in the FreeMarker
110114
* {@code Configuration}, FreeMarker will read template files using the platform
111115
* file encoding (defined by the JVM system property {@code file.encoding})
112-
* or {@code "utf-8"} if the platform file encoding is undefined.
116+
* or UTF-8 if the platform file encoding is undefined.
113117
* <p>It's recommended to specify the encoding in the FreeMarker {@code Configuration}
114118
* rather than per template if all your templates share a common encoding.
115-
* <p>Note that the specified or default encoding is not used for template
116-
* rendering. Instead, an explicit encoding must be specified for the rendering
117-
* process. See the note in the {@linkplain FreeMarkerView class-level
118-
* documentation} for details.
119+
* <p>See the note in the {@linkplain FreeMarkerView class-level documentation}
120+
* for details regarding the encoding used to render the response.
119121
* @see freemarker.template.Configuration#setDefaultEncoding
120122
* @see #setCharset(Charset)
121123
* @see #getEncoding()

spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616

1717
package org.springframework.web.servlet.view.freemarker;
1818

19+
import java.util.Locale;
20+
21+
import freemarker.template.Configuration;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.StringUtils;
25+
import org.springframework.web.servlet.View;
1926
import org.springframework.web.servlet.view.AbstractTemplateViewResolver;
2027
import org.springframework.web.servlet.view.AbstractUrlBasedView;
2128

@@ -29,12 +36,19 @@
2936
* <p><b>Note:</b> To ensure that the correct encoding is used when the rendering
3037
* the response, set the {@linkplain #setContentType(String) content type} with an
3138
* appropriate {@code charset} attribute &mdash; for example,
32-
* {@code "text/html;charset=UTF-8"}.
39+
* {@code "text/html;charset=UTF-8"}; however, as of Spring Framework 6.2, it is
40+
* no longer strictly necessary to explicitly set the content type in the
41+
* {@code FreeMarkerViewResolver} if you have set an explicit encoding via either
42+
* {@link FreeMarkerView#setEncoding(String)},
43+
* {@link FreeMarkerConfigurer#setDefaultEncoding(String)}, or
44+
* {@link Configuration#setDefaultEncoding(String)}.
3345
*
3446
* <p><b>Note:</b> When chaining ViewResolvers, a {@code FreeMarkerViewResolver} will
3547
* check for the existence of the specified template resources and only return
3648
* a non-null {@code View} object if the template was actually found.
3749
*
50+
* <p>Note: Spring's FreeMarker support requires FreeMarker 2.3.26 or higher.
51+
*
3852
* @author Juergen Hoeller
3953
* @author Sam Brannen
4054
* @since 1.1
@@ -83,4 +97,65 @@ protected AbstractUrlBasedView instantiateView() {
8397
return (getViewClass() == FreeMarkerView.class ? new FreeMarkerView() : super.instantiateView());
8498
}
8599

100+
/**
101+
* Delegates to {@code super.loadView(viewName, locale)} for standard behavior
102+
* and then to {@link #postProcessView(FreeMarkerView)} for customization.
103+
* @since 6.2
104+
* @see org.springframework.web.servlet.view.UrlBasedViewResolver#loadView(String, Locale)
105+
* @see #postProcessView(FreeMarkerView)
106+
*/
107+
@Override
108+
@Nullable
109+
protected View loadView(String viewName, Locale locale) throws Exception {
110+
View view = super.loadView(viewName, locale);
111+
if (view instanceof FreeMarkerView freeMarkerView) {
112+
postProcessView(freeMarkerView);
113+
}
114+
return view;
115+
}
116+
117+
/**
118+
* Post process the supplied {@link FreeMarkerView} after it has been {@linkplain
119+
* org.springframework.web.servlet.view.UrlBasedViewResolver#loadView(String, Locale)
120+
* loaded}.
121+
* <p>The default implementation attempts to override the
122+
* {@linkplain org.springframework.web.servlet.view.AbstractView#setContentType(String)
123+
* content type} of the view with {@code "text/html;charset=<encoding>"},
124+
* where {@code <encoding>} is equal to an explicitly configured character
125+
* encoding for the underlying FreeMarker template file. If an explicit content
126+
* type has been configured for this view resolver or if no explicit character
127+
* encoding has been configured for the template file, this method does not
128+
* modify the supplied {@code FreeMarkerView}.
129+
* @since 6.2
130+
* @see #loadView(String, Locale)
131+
* @see #setContentType(String)
132+
* @see org.springframework.web.servlet.view.AbstractView#setContentType(String)
133+
*/
134+
protected void postProcessView(FreeMarkerView freeMarkerView) {
135+
// If an explicit content type has been configured for all views, it has
136+
// already been set in the view in UrlBasedViewResolver#buildView(String),
137+
// and there is no need to override it here.
138+
if (getContentType() != null) {
139+
return;
140+
}
141+
142+
// Check if the view has an explicit encoding set.
143+
String encoding = freeMarkerView.getEncoding();
144+
if (encoding == null) {
145+
// If an explicit encoding has not been configured for this particular view,
146+
// use the explicit default encoding for the FreeMarker Configuration, if set.
147+
Configuration configuration = freeMarkerView.obtainConfiguration();
148+
if (configuration.isDefaultEncodingExplicitlySet()) {
149+
encoding = configuration.getDefaultEncoding();
150+
}
151+
}
152+
if (StringUtils.hasText(encoding)) {
153+
String contentType = "text/html;charset=" + encoding;
154+
if (logger.isDebugEnabled()) {
155+
logger.debug("Setting Content-Type for view [%s] to: %s".formatted(freeMarkerView, contentType));
156+
}
157+
freeMarkerView.setContentType(contentType);
158+
}
159+
}
160+
86161
}

spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ViewResolutionIntegrationTests.java

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ static void verifyDefaultFileEncoding() {
5757
@Nested
5858
class FreeMarkerTests {
5959

60+
private static final String DEFAULT_ENCODING = "ISO-8859-1";
61+
6062
private static final String EXPECTED_BODY = """
6163
<html>
6264
<body>
@@ -74,48 +76,37 @@ void freemarkerWithInvalidConfig() {
7476
}
7577

7678
@Test
77-
void freemarkerWithDefaults() throws Exception {
78-
String encoding = "ISO-8859-1";
79-
MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class);
80-
assertThat(response.isCharset()).as("character encoding set in response").isTrue();
81-
assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding));
82-
// Prior to Spring Framework 6.2, the charset is not updated in the Content-Type.
83-
// Thus, we expect ISO-8859-1 instead of UTF-8.
84-
assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
85-
assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
79+
void freemarkerWithDefaultEncoding() throws Exception {
80+
// Since no explicit encoding or content type has been set, we expect ISO-8859-1,
81+
// which is the default.
82+
runTestAndAssertResults(DEFAULT_ENCODING, FreeMarkerDefaultEncodingConfig.class);
8683
}
8784

8885
@Test // gh-16629, gh-33071
89-
void freemarkerWithExistingViewResolver() throws Exception {
90-
String encoding = "ISO-8859-1";
91-
MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class);
92-
assertThat(response.isCharset()).as("character encoding set in response").isTrue();
93-
assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding));
94-
// Prior to Spring Framework 6.2, the charset is not updated in the Content-Type.
95-
// Thus, we expect ISO-8859-1 instead of UTF-8.
96-
assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
97-
assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
86+
void freemarkerWithExistingViewResolverWithDefaultEncoding() throws Exception {
87+
// Since no explicit encoding or content type has been set, we expect ISO-8859-1,
88+
// which is the default.
89+
runTestAndAssertResults(DEFAULT_ENCODING, ExistingViewResolverConfig.class);
9890
}
9991

100-
@Test // gh-33071
92+
@Test // gh-33071, gh-33119
10193
void freemarkerWithExplicitDefaultEncoding() throws Exception {
102-
String encoding = "ISO-8859-1";
103-
MockHttpServletResponse response = runTest(ExplicitDefaultEncodingConfig.class);
104-
assertThat(response.isCharset()).as("character encoding set in response").isTrue();
105-
assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding));
106-
// Prior to Spring Framework 6.2, the charset is not updated in the Content-Type.
107-
// Thus, we expect ISO-8859-1 instead of UTF-8.
108-
assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
109-
assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
94+
// As of Spring Framework 6.2, the charset is automatically updated in the Content-Type, as
95+
// long as the user didn't configure the Content-Type directly in the FreeMarkerViewResolver.
96+
runTestAndAssertResults("UTF-8", ExplicitDefaultEncodingConfig.class);
11097
}
11198

11299
@Test // gh-33071
113100
void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception {
114-
String encoding = "UTF-16";
115-
MockHttpServletResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class);
101+
// When the Content-Type is explicitly set on the view resolver, it should be used.
102+
runTestAndAssertResults("UTF-16", ExplicitDefaultEncodingAndContentTypeConfig.class);
103+
}
104+
105+
106+
private static void runTestAndAssertResults(String encoding, Class<?> configClass) throws Exception {
107+
MockHttpServletResponse response = runTest(configClass);
116108
assertThat(response.isCharset()).as("character encoding set in response").isTrue();
117109
assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY.formatted(encoding));
118-
// When the Content-Type is explicitly set on the view resolver, it should be used.
119110
assertThat(response.getCharacterEncoding()).isEqualTo(encoding);
120111
assertThat(response.getContentType()).isEqualTo("text/html;charset=" + encoding);
121112
}
@@ -131,7 +122,7 @@ public void configureViewResolvers(ViewResolverRegistry registry) {
131122
}
132123

133124
@Configuration
134-
static class FreeMarkerWebConfig extends AbstractWebConfig {
125+
static class FreeMarkerDefaultEncodingConfig extends AbstractWebConfig {
135126

136127
@Override
137128
public void configureViewResolvers(ViewResolverRegistry registry) {

0 commit comments

Comments
 (0)