From d133ab60ee81582826af7e68dbf8a060e37d267e Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:43:50 +0200 Subject: [PATCH] Improve documentation regarding encoding in FreeMarker support This commit also introduces integration tests to test the status quo regarding encoding. Closes gh-33071 --- .../FreeMarkerConfigurationFactory.java | 132 +++++---- .../FreeMarkerConfigurationFactoryBean.java | 21 +- .../freemarker/FreeMarkerTemplateUtils.java | 11 +- .../ui/freemarker/SpringTemplateLoader.java | 12 +- .../view/freemarker/FreeMarkerConfig.java | 13 +- .../view/freemarker/FreeMarkerConfigurer.java | 34 ++- .../view/freemarker/FreeMarkerView.java | 69 ++++- ...WebFluxViewResolutionIntegrationTests.java | 207 +++++++++++++ .../web/reactive/config/index_ISO-8859-1.ftl | 1 + .../web/reactive/config/index_UTF-8.ftl | 1 + .../view/freemarker/FreeMarkerConfig.java | 11 +- .../view/freemarker/FreeMarkerConfigurer.java | 51 ++-- .../view/freemarker/FreeMarkerView.java | 99 ++++--- .../freemarker/FreeMarkerViewResolver.java | 15 +- .../ViewResolutionIntegrationTests.java | 280 +++++++++++------- .../config/annotation/WEB-INF/index.ftl | 2 +- .../config/annotation/WEB-INF/index.tpl | 2 +- 17 files changed, 681 insertions(+), 280 deletions(-) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java create mode 100644 spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_ISO-8859-1.ftl create mode 100644 spring-webflux/src/test/resources/org/springframework/web/reactive/config/index_UTF-8.ftl diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java index bc3ddc27e01..c3df6a706cf 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -41,9 +41,11 @@ import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** - * Factory that configures a FreeMarker Configuration. Can be used standalone, but - * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a - * Configuration as bean reference, or FreeMarkerConfigurer for web views. + * Factory that configures a FreeMarker {@link Configuration}. + * + *
Can be used standalone, but typically you will either use + * {@link FreeMarkerConfigurationFactoryBean} for preparing a {@code Configuration} + * as a bean reference, or {@code FreeMarkerConfigurer} for web views. * *
The optional "configLocation" property sets the location of a FreeMarker * properties file, within the current application. FreeMarker properties can be @@ -52,17 +54,18 @@ import org.springframework.util.CollectionUtils; * subject to constraints set by FreeMarker. * *
The "freemarkerVariables" property can be used to specify a Map of - * shared variables that will be applied to the Configuration via the + * shared variables that will be applied to the {@code Configuration} via the * {@code setAllSharedVariables()} method. Like {@code setSettings()}, * these entries are subject to FreeMarker constraints. * *
The simplest way to use this class is to specify a "templateLoaderPath"; * FreeMarker does not need any further configuration then. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Darren Davison * @author Juergen Hoeller + * @author Sam Brannen * @since 03.03.2004 * @see #setConfigLocation * @see #setFreemarkerSettings @@ -107,7 +110,7 @@ public class FreeMarkerConfigurationFactory { /** * Set the location of the FreeMarker config file. - * Alternatively, you can specify all setting locally. + *
Alternatively, you can specify all settings locally. * @see #setFreemarkerSettings * @see #setTemplateLoaderPath */ @@ -134,25 +137,33 @@ public class FreeMarkerConfigurationFactory { } /** - * Set the default encoding for the FreeMarker configuration. - * If not specified, FreeMarker will use the platform file encoding. - *
Used for template rendering unless there is an explicit encoding specified - * for the rendering process (for example, on Spring's FreeMarkerView). + * Set the default encoding for the FreeMarker {@link Configuration}, which + * is used to decode byte sequences to character sequences when reading template + * files. + *
If not specified, FreeMarker will read template files using the platform + * file encoding (defined by the JVM system property {@code file.encoding}) + * or {@code "utf-8"} if the platform file encoding is undefined. + *
Note that the encoding is not used for template rendering. Instead, an + * explicit encoding must be specified for the rendering process — for + * example, via Spring's {@code FreeMarkerView} or {@code FreeMarkerViewResolver}. * @see freemarker.template.Configuration#setDefaultEncoding * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setContentType + * @see org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver#setContentType */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** - * Set a List of {@code TemplateLoader}s that will be used to search - * for templates. For example, one or more custom loaders such as database - * loaders could be configured and injected here. - *
The {@link TemplateLoader TemplateLoaders} specified here will be - * registered before the default template loaders that this factory - * registers (such as loaders for specified "templateLoaderPaths" or any - * loaders registered in {@link #postProcessTemplateLoaders}). + * Set a List of {@link TemplateLoader TemplateLoaders} that will be used to + * search for templates. + *
For example, one or more custom loaders such as database loaders could + * be configured and injected here. + *
The {@code TemplateLoaders} specified here will be registered before + * the default template loaders that this factory registers (such as loaders + * for specified "templateLoaderPaths" or any loaders registered in + * {@link #postProcessTemplateLoaders}). * @see #setTemplateLoaderPaths * @see #postProcessTemplateLoaders */ @@ -161,13 +172,14 @@ public class FreeMarkerConfigurationFactory { } /** - * Set a List of {@code TemplateLoader}s that will be used to search - * for templates. For example, one or more custom loaders such as database - * loaders can be configured. - *
The {@link TemplateLoader TemplateLoaders} specified here will be - * registered after the default template loaders that this factory - * registers (such as loaders for specified "templateLoaderPaths" or any - * loaders registered in {@link #postProcessTemplateLoaders}). + * Set a List of {@link TemplateLoader TemplateLoaders} that will be used to + * search for templates. + *
For example, one or more custom loaders such as database loaders could + * be configured and injected here. + *
The {@code TemplateLoaders} specified here will be registered after + * the default template loaders that this factory registers (such as loaders + * for specified "templateLoaderPaths" or any loaders registered in + * {@link #postProcessTemplateLoaders}). * @see #setTemplateLoaderPaths * @see #postProcessTemplateLoaders */ @@ -177,7 +189,7 @@ public class FreeMarkerConfigurationFactory { /** * Set the Freemarker template loader path via a Spring resource location. - * See the "templateLoaderPaths" property for details on path handling. + *
See the "templateLoaderPaths" property for details on path handling. * @see #setTemplateLoaderPaths */ public void setTemplateLoaderPath(String templateLoaderPath) { @@ -188,28 +200,29 @@ public class FreeMarkerConfigurationFactory { * Set multiple Freemarker template loader paths via Spring resource locations. *
When populated via a String, standard URLs like "file:" and "classpath:" * pseudo URLs are supported, as understood by ResourceEditor. Allows for - * relative paths when running in an ApplicationContext. - *
Will define a path for the default FreeMarker template loader. - * If a specified resource cannot be resolved to a {@code java.io.File}, - * a generic SpringTemplateLoader will be used, without modification detection. - *
To enforce the use of SpringTemplateLoader, i.e. to not resolve a path - * as file system resource in any case, turn off the "preferFileSystemAccess" + * relative paths when running in an {@code ApplicationContext}. + *
Will define a path for the default FreeMarker template loader. If a + * specified resource cannot be resolved to a {@code java.io.File}, a generic + * {@link SpringTemplateLoader} will be used, without modification detection. + *
To enforce the use of {@code SpringTemplateLoader}, i.e. to not resolve + * a path as file system resource in any case, turn off the "preferFileSystemAccess" * flag. See the latter's javadoc for details. *
If you wish to specify your own list of TemplateLoaders, do not set this - * property and instead use {@code setTemplateLoaders(List templateLoaders)} + * property and instead use {@link #setPostTemplateLoaders(TemplateLoader...)}. * @see org.springframework.core.io.ResourceEditor * @see org.springframework.context.ApplicationContext#getResource * @see freemarker.template.Configuration#setDirectoryForTemplateLoading * @see SpringTemplateLoader + * @see #setPreferFileSystemAccess(boolean) */ public void setTemplateLoaderPaths(String... templateLoaderPaths) { this.templateLoaderPaths = templateLoaderPaths; } /** - * Set the Spring ResourceLoader to use for loading FreeMarker template files. - * The default is DefaultResourceLoader. Will get overridden by the - * ApplicationContext if running in a context. + * Set the {@link ResourceLoader} to use for loading FreeMarker template files. + *
The default is {@link DefaultResourceLoader}. Will get overridden by the + * {@code ApplicationContext} if running in a context. * @see org.springframework.core.io.DefaultResourceLoader */ public void setResourceLoader(ResourceLoader resourceLoader) { @@ -217,7 +230,7 @@ public class FreeMarkerConfigurationFactory { } /** - * Return the Spring ResourceLoader to use for loading FreeMarker template files. + * Return the {@link ResourceLoader} to use for loading FreeMarker template files. */ protected ResourceLoader getResourceLoader() { return this.resourceLoader; @@ -225,11 +238,11 @@ public class FreeMarkerConfigurationFactory { /** * Set whether to prefer file system access for template loading. - * File system access enables hot detection of template changes. + *
File system access enables hot detection of template changes. *
If this is enabled, FreeMarkerConfigurationFactory will try to resolve * the specified "templateLoaderPath" as file system resource (which will work * for expanded class path resources and ServletContext resources too). - *
Default is "true". Turn this off to always load via SpringTemplateLoader + *
Default is "true". Turn this off to always load via {@link SpringTemplateLoader} * (i.e. as stream, without hot detection of template changes), which might * be necessary if some of your templates reside in an expanded classes * directory while others reside in jar files. @@ -248,8 +261,8 @@ public class FreeMarkerConfigurationFactory { /** - * Prepare the FreeMarker Configuration and return it. - * @return the FreeMarker Configuration object + * Prepare the FreeMarker {@link Configuration} and return it. + * @return the FreeMarker {@code Configuration} object * @throws IOException if the config file wasn't found * @throws TemplateException on FreeMarker initialization failure */ @@ -314,11 +327,12 @@ public class FreeMarkerConfigurationFactory { } /** - * Return a new Configuration object. Subclasses can override this for custom - * initialization (e.g. specifying a FreeMarker compatibility level which is a - * new feature in FreeMarker 2.3.21), or for using a mock object for testing. - *
Called by {@code createConfiguration()}. - * @return the Configuration object + * Return a new {@link Configuration} object. + *
Subclasses can override this for custom initialization — for example, + * to specify a FreeMarker compatibility level (which is a new feature in + * FreeMarker 2.3.21), or to use a mock object for testing. + *
Called by {@link #createConfiguration()}. + * @return the {@code Configuration} object * @throws IOException if a config file wasn't found * @throws TemplateException on FreeMarker initialization failure * @see #createConfiguration() @@ -328,11 +342,11 @@ public class FreeMarkerConfigurationFactory { } /** - * Determine a FreeMarker TemplateLoader for the given path. - *
Default implementation creates either a FileTemplateLoader or - * a SpringTemplateLoader. + * Determine a FreeMarker {@link TemplateLoader} for the given path. + *
Default implementation creates either a {@link FileTemplateLoader} or + * a {@link SpringTemplateLoader}. * @param templateLoaderPath the path to load templates from - * @return an appropriate TemplateLoader + * @return an appropriate {@code TemplateLoader} * @see freemarker.cache.FileTemplateLoader * @see SpringTemplateLoader */ @@ -366,9 +380,9 @@ public class FreeMarkerConfigurationFactory { /** * To be overridden by subclasses that want to register custom - * TemplateLoader instances after this factory created its default + * {@link TemplateLoader} instances after this factory created its default * template loaders. - *
Called by {@code createConfiguration()}. Note that specified + *
Called by {@link #createConfiguration()}. Note that specified * "postTemplateLoaders" will be registered after any loaders * registered by this callback; as a consequence, they are not * included in the given List. @@ -381,10 +395,10 @@ public class FreeMarkerConfigurationFactory { } /** - * Return a TemplateLoader based on the given TemplateLoader list. - * If more than one TemplateLoader has been registered, a FreeMarker - * MultiTemplateLoader needs to be created. - * @param templateLoaders the final List of TemplateLoader instances + * Return a {@link TemplateLoader} based on the given {@code TemplateLoader} list. + *
If more than one TemplateLoader has been registered, a FreeMarker + * {@link MultiTemplateLoader} will be created. + * @param templateLoaders the final List of {@code TemplateLoader} instances * @return the aggregate TemplateLoader */ @Nullable @@ -404,10 +418,10 @@ public class FreeMarkerConfigurationFactory { /** * To be overridden by subclasses that want to perform custom - * post-processing of the Configuration object after this factory + * post-processing of the {@link Configuration} object after this factory * performed its default initialization. - *
Called by {@code createConfiguration()}. - * @param config the current Configuration object + *
Called by {@link #createConfiguration()}. + * @param config the current {@code Configuration} object * @throws IOException if a config file wasn't found * @throws TemplateException on FreeMarker initialization failure * @see #createConfiguration() diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java index 4f7b42bc1c5..3aac7d4df21 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java @@ -27,22 +27,25 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.lang.Nullable; /** - * Factory bean that creates a FreeMarker Configuration and provides it as - * bean reference. This bean is intended for any kind of usage of FreeMarker - * in application code, e.g. for generating email content. For web views, - * FreeMarkerConfigurer is used to set up a FreeMarkerConfigurationFactory. - *
- * The simplest way to use this class is to specify just a "templateLoaderPath"; + * Factory bean that creates a FreeMarker {@link Configuration} and provides it + * as a bean reference. + * + *
This bean is intended for any kind of usage of FreeMarker in application + * code — for example, for generating email content. For web views, + * {@code FreeMarkerConfigurer} is used to set up a {@link FreeMarkerConfigurationFactory}. + * + *
The simplest way to use this class is to specify just a "templateLoaderPath"; * you do not need any further configuration then. For example, in a web * application context: * *
<bean id="freemarkerConfiguration" class="org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean"> * <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/> * </bean>- - * See the base class FreeMarkerConfigurationFactory for configuration details. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
See the {@link FreeMarkerConfigurationFactory} base class for configuration + * details. + * + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Darren Davison * @since 03.03.2004 diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java index e1ca06bf99e..022ebd9bdec 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerTemplateUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -24,7 +24,8 @@ import freemarker.template.TemplateException; /** * Utility class for working with FreeMarker. - * Provides convenience methods to process a FreeMarker template with a model. + * + *
Provides convenience methods to process a FreeMarker template with a model. * * @author Juergen Hoeller * @since 14.03.2004 @@ -33,12 +34,12 @@ public abstract class FreeMarkerTemplateUtils { /** * Process the specified FreeMarker template with the given model and write - * the result to the given Writer. - *
When using this method to prepare a text for a mail to be sent with Spring's + * the result to a String. + *
When using this method to prepare text for a mail to be sent with Spring's * mail support, consider wrapping IO/TemplateException in MailPreparationException. * @param model the model object, typically a Map that contains model names * as keys and model objects as values - * @return the result as String + * @return the result as a String * @throws IOException if the template wasn't found or couldn't be read * @throws freemarker.template.TemplateException if rendering failed * @see org.springframework.mail.MailPreparationException diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java index d4140aa681f..1749b0a37d9 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -29,9 +29,11 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.lang.Nullable; /** - * FreeMarker {@link TemplateLoader} adapter that loads via a Spring {@link ResourceLoader}. - * Used by {@link FreeMarkerConfigurationFactory} for any resource loader path that cannot - * be resolved to a {@link java.io.File}. + * FreeMarker {@link TemplateLoader} adapter that loads template files via a + * Spring {@link ResourceLoader}. + * + *
Used by {@link FreeMarkerConfigurationFactory} for any resource loader path + * that cannot be resolved to a {@link java.io.File}. * * @author Juergen Hoeller * @since 14.03.2004 @@ -48,7 +50,7 @@ public class SpringTemplateLoader implements TemplateLoader { /** - * Create a new SpringTemplateLoader. + * Create a new {@code SpringTemplateLoader}. * @param resourceLoader the Spring ResourceLoader to use * @param templateLoaderPath the template loader path to use */ diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java index 5ed92f4426d..e30c3d9b601 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2024 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. @@ -20,8 +20,9 @@ import freemarker.template.Configuration; /** * Interface to be implemented by objects that configure and manage a - * FreeMarker Configuration object in a web environment. Detected and - * used by {@link FreeMarkerView}. + * FreeMarker {@link Configuration} object in a web environment. + * + *
Detected and used by {@link FreeMarkerView}. * * @author Rossen Stoyanchev * @since 5.0 @@ -29,11 +30,11 @@ import freemarker.template.Configuration; public interface FreeMarkerConfig { /** - * Return the FreeMarker Configuration object for the current + * Return the FreeMarker {@link Configuration} object for the current * web application context. - *
A FreeMarker Configuration object may be used to set FreeMarker + *
A FreeMarker {@code Configuration} object may be used to set FreeMarker * properties and shared objects, and allows to retrieve templates. - * @return the FreeMarker Configuration + * @return the FreeMarker {@code Configuration} */ Configuration getConfiguration(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java index 179e9c319c2..2159dbfb9f8 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/freemarker/FreeMarkerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2024 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. @@ -31,9 +31,10 @@ import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; import org.springframework.util.Assert; /** - * Configures FreeMarker for web usage via the "configLocation" and/or - * "freemarkerSettings" and/or "templateLoaderPath" properties. - * The simplest way to use this class is to specify just a "templateLoaderPath" + * Configures FreeMarker for web usage via the "configLocation", + * "freemarkerSettings", or "templateLoaderPath" properties. + * + *
The simplest way to use this class is to specify just a "templateLoaderPath" * (e.g. "classpath:templates"); you do not need any further configuration then. * *
This bean must be included in the application context of any application @@ -42,9 +43,9 @@ import org.springframework.util.Assert; * by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to be found by * {@code FreeMarkerView} without depending on the bean name of the configurer. * - *
Note that you can also refer to a pre-configured FreeMarker Configuration + *
Note that you can also refer to a pre-configured FreeMarker {@code Configuration} * instance via the "configuration" property. This allows to share a FreeMarker - * Configuration for web and email usage for example. + * {@code Configuration} for web and email usage for example. * *
This configurer registers a template loader for this package, allowing to * reference the "spring.ftl" macro library contained in this package: @@ -54,7 +55,7 @@ import org.springframework.util.Assert; * <@spring.bind "person.age"/> * age is ${spring.status.value} * - * Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Rossen Stoyanchev * @since 5.0 @@ -72,10 +73,10 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Set a pre-configured Configuration to use for the FreeMarker web config, - * e.g. a shared one for web and email usage. If this is not set, - * FreeMarkerConfigurationFactory's properties (inherited by this class) - * have to be specified. + * Set a preconfigured {@link Configuration} to use for the FreeMarker web + * config — for example, a shared one for web and email usage. + *
If this is not set, FreeMarkerConfigurationFactory's properties (inherited + * by this class) have to be specified. */ public void setConfiguration(Configuration configuration) { this.configuration = configuration; @@ -83,9 +84,10 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Initialize FreeMarkerConfigurationFactory's Configuration - * if not overridden by a pre-configured FreeMarker Configuration. - *
Sets up a ClassTemplateLoader to use for loading Spring macros. + * Initialize FreeMarkerConfigurationFactory's {@link Configuration} + * if not overridden by a pre-configured FreeMarker {@link Configuration}. + *
Indirectly sets up a {@link ClassTemplateLoader} to use for loading + * Spring macros. * @see #createConfiguration * @see #setConfiguration */ @@ -97,7 +99,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory } /** - * This implementation registers an additional ClassTemplateLoader + * This implementation registers an additional {@link ClassTemplateLoader} * for the Spring-provided macros, added to the end of the list. */ @Override @@ -107,7 +109,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Return the Configuration object wrapped by this bean. + * Return the {@link Configuration} object wrapped by this bean. */ @Override public Configuration getConfiguration() { 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 2afeb1fafb6..ff1af66f9ac 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -56,16 +56,40 @@ import org.springframework.web.server.ServerWebExchange; /** * A {@code View} implementation that uses the FreeMarker template engine. * + *
Exposes the following configuration properties: + *
Depends on a single {@link FreeMarkerConfig} object such as * {@link FreeMarkerConfigurer} being accessible in the application context. - * Alternatively the FreeMarker {@link Configuration} can be set directly on this - * class via {@link #setConfiguration}. + * Alternatively the FreeMarker {@link Configuration} can be set directly via + * {@link #setConfiguration}. + * + *
Note: To ensure that the correct encoding is used when rendering the + * response as well as when the client reads the response, the following steps + * must be taken. + *
The {@link #setUrl(String) url} property is the location of the FreeMarker - * template relative to the FreeMarkerConfigurer's - * {@link FreeMarkerConfigurer#setTemplateLoaderPath templateLoaderPath}. + * Note, however, that {@link FreeMarkerConfigurer} sets the default encoding in + * the FreeMarker {@code Configuration} to "UTF-8" and that + * {@link org.springframework.web.reactive.result.view.AbstractView AbstractView} + * sets the supported media type to {@code "text/html;charset=UTF-8"} by default. + * Thus, those default values are likely suitable for most applications. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Rossen Stoyanchev * @author Sam Brannen @@ -124,18 +148,37 @@ public class FreeMarkerView extends AbstractUrlBasedView { } /** - * Set the encoding of the FreeMarker template file. - *
By default {@link FreeMarkerConfigurer} sets the default encoding in - * the FreeMarker configuration to "UTF-8". It's recommended to specify the - * encoding in the FreeMarker {@link Configuration} rather than per template - * if all your templates share a common encoding. + * Set the encoding used to decode byte sequences to character sequences when + * reading the FreeMarker template file for this view. + *
Defaults to {@code null} to signal that the FreeMarker + * {@link Configuration} should be used to determine the encoding. + *
A non-null encoding will override the default encoding determined by + * the FreeMarker {@code Configuration}. + *
If the encoding is not explicitly set here or in the FreeMarker + * {@code Configuration}, FreeMarker will read template files using the platform + * file encoding (defined by the JVM system property {@code file.encoding}) + * or {@code "utf-8"} if the platform file encoding is undefined. Note, + * however, that {@link FreeMarkerConfigurer} sets the default encoding in the + * FreeMarker {@code Configuration} to "UTF-8". + *
It's recommended to specify the encoding in the FreeMarker {@code Configuration} + * rather than per template if all your templates share a common encoding. + *
Note that the specified or default encoding is not used for template + * rendering. Instead, an explicit encoding must be specified for the rendering + * process. See the note in the {@linkplain FreeMarkerView class-level + * documentation} for details. + * @see freemarker.template.Configuration#setDefaultEncoding + * @see #getEncoding() */ public void setEncoding(@Nullable String encoding) { this.encoding = encoding; } /** - * Get the encoding for the FreeMarker template. + * Get the encoding used to decode byte sequences to character sequences + * when reading the FreeMarker template file for this view, or {@code null} + * to signal that the FreeMarker {@link Configuration} should be used to + * determine the encoding. + * @see #setEncoding(String) */ @Nullable protected String getEncoding() { 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 new file mode 100644 index 00000000000..df1ccbd312b --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxViewResolutionIntegrationTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2024 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 + * + * https://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.config; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import freemarker.cache.ClassTemplateLoader; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewResolver; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.testfixture.server.MockServerWebExchange; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatRuntimeException; + +/** + * Integration tests for view resolution with {@code @EnableWebFlux}. + * + * @author Sam Brannen + * @since 6.1.11 + * @see org.springframework.web.servlet.config.annotation.ViewResolutionIntegrationTests + */ +class WebFluxViewResolutionIntegrationTests { + + private static final MediaType TEXT_HTML_UTF8 = MediaType.parseMediaType("text/html;charset=UTF-8"); + + 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 ClassTemplateLoader classTemplateLoader = + new ClassTemplateLoader(WebFluxViewResolutionIntegrationTests.class, ""); + + @Test + void freemarkerWithInvalidConfig() { + assertThatRuntimeException() + .isThrownBy(() -> runTest(InvalidFreeMarkerWebFluxConfig.class)) + .withMessageContaining("In addition to a FreeMarker view resolver "); + } + + @Test + void freemarkerWithDefaults() throws Exception { + MockServerHttpResponse response = runTest(FreeMarkerWebFluxConfig.class); + StepVerifier.create(response.getBodyAsString()).expectNext(EXPECTED_BODY).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(); + 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(); + // 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); + } + + + @EnableWebFlux + @Configuration(proxyBeanMethods = false) + static class InvalidFreeMarkerWebFluxConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + } + + @Configuration(proxyBeanMethods = false) + static class FreeMarkerWebFluxConfig extends AbstractWebFluxConfig { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreTemplateLoaders(classTemplateLoader); + return configurer; + } + } + + @Configuration(proxyBeanMethods = false) + static class ExplicitDefaultEncodingConfig extends AbstractWebFluxConfig { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreTemplateLoaders(classTemplateLoader); + configurer.setDefaultEncoding(UTF_8.name()); + return configurer; + } + } + + @Configuration(proxyBeanMethods = false) + static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebFluxConfig { + + @Autowired + ApplicationContext applicationContext; + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + FreeMarkerViewResolver resolver = new FreeMarkerViewResolver("", ".ftl"); + resolver.setSupportedMediaTypes(List.of(TEXT_HTML_ISO_8859_1)); + resolver.setApplicationContext(this.applicationContext); + registry.viewResolver(resolver); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setPreTemplateLoaders(classTemplateLoader); + configurer.setDefaultEncoding(ISO_8859_1.name()); + return configurer; + } + + @Override + @Bean + public SampleController sampleController() { + return new SampleController("index_ISO-8859-1"); + } + } + } + + + private static MockServerHttpResponse runTest(Class> configClass) throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configClass); + MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); + new DispatcherHandler(context).handle(exchange).block(Duration.ofSeconds(1)); + return exchange.getResponse(); + } + + + @EnableWebFlux + abstract static class AbstractWebFluxConfig implements WebFluxConfigurer { + + @Bean + public SampleController sampleController() { + return new SampleController("index_UTF-8"); + } + } + + @Controller + static class SampleController { + + private final String viewName; + + SampleController(String viewName) { + this.viewName = viewName; + } + + @GetMapping("/") + String index(MapDetected and used by {@link FreeMarkerView}. * * @author Darren Davison * @author Rob Harrop @@ -34,9 +35,9 @@ public interface FreeMarkerConfig { /** * Return the FreeMarker {@link Configuration} object for the current * web application context. - *
A FreeMarker Configuration object may be used to set FreeMarker + *
A FreeMarker {@code Configuration} object may be used to set FreeMarker * properties and shared objects, and allows to retrieve templates. - * @return the FreeMarker Configuration + * @return the FreeMarker {@code Configuration} */ Configuration getConfiguration(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java index b1604ed1d6c..3943bd1898b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 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. @@ -31,9 +31,10 @@ import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; import org.springframework.util.Assert; /** - * JavaBean to configure FreeMarker for web usage, via the "configLocation" - * and/or "freemarkerSettings" and/or "templateLoaderPath" properties. - * The simplest way to use this class is to specify just a "templateLoaderPath"; + * Bean to configure FreeMarker for web usage, via the "configLocation", + * "freemarkerSettings", or "templateLoaderPath" properties. + * + *
The simplest way to use this class is to specify just a "templateLoaderPath"; * you do not need any further configuration then. * *
@@ -41,17 +42,17 @@ import org.springframework.util.Assert; * <property name="templateLoaderPath"><value>/WEB-INF/freemarker/</value></property> * </bean>* - * This bean must be included in the application context of any application - * using Spring's FreeMarkerView for web MVC. It exists purely to configure FreeMarker. - * It is not meant to be referenced by application components but just internally - * by FreeMarkerView. Implements FreeMarkerConfig to be found by FreeMarkerView without - * depending on the bean name of the configurer. Each DispatcherServlet can define its - * own FreeMarkerConfigurer if desired. + *
This bean must be included in the application context of any application + * using Spring's {@link FreeMarkerView} for web MVC. It exists purely to configure + * FreeMarker. It is not meant to be referenced by application components but just + * internally by {@code FreeMarkerView}. Implements {@link FreeMarkerConfig} to + * be found by {@code FreeMarkerView} without depending on the bean name of the + * configurer. Each DispatcherServlet can define its own {@code FreeMarkerConfigurer} + * if desired. * - *
Note that you can also refer to a preconfigured FreeMarker Configuration - * instance, for example one set up by FreeMarkerConfigurationFactoryBean, via - * the "configuration" property. This allows to share a FreeMarker Configuration - * for web and email usage, for example. + *
Note that you can also refer to a pre-configured FreeMarker {@code Configuration} + * instance via the "configuration" property. This allows to share a FreeMarker + * {@code Configuration} for web and email usage for example. * *
This configurer registers a template loader for this package, allowing to * reference the "spring.ftl" macro library contained in this package: @@ -61,7 +62,7 @@ import org.springframework.util.Assert; * <@spring.bind "person.age"/> * age is ${spring.status.value} * - * Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * * @author Darren Davison * @author Rob Harrop @@ -81,10 +82,10 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Set a preconfigured Configuration to use for the FreeMarker web config, e.g. a - * shared one for web and email usage, set up via FreeMarkerConfigurationFactoryBean. - * If this is not set, FreeMarkerConfigurationFactory's properties (inherited by - * this class) have to be specified. + * Set a preconfigured {@link Configuration} to use for the FreeMarker web + * config — for example, a shared one for web and email usage. + *
If this is not set, FreeMarkerConfigurationFactory's properties (inherited + * by this class) have to be specified. * @see org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean */ public void setConfiguration(Configuration configuration) { @@ -93,11 +94,13 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Initialize FreeMarkerConfigurationFactory's Configuration - * if not overridden by a preconfigured FreeMarker Configuration. - *
Sets up a ClassTemplateLoader to use for loading Spring macros. + * Initialize FreeMarkerConfigurationFactory's {@link Configuration} + * if not overridden by a preconfigured FreeMarker {@code Configuration}. + *
Indirectly sets up a {@link ClassTemplateLoader} to use for loading + * Spring macros. * @see #createConfiguration * @see #setConfiguration + * @see #postProcessTemplateLoaders(List) */ @Override public void afterPropertiesSet() throws IOException, TemplateException { @@ -107,7 +110,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory } /** - * This implementation registers an additional ClassTemplateLoader + * This implementation registers an additional {@link ClassTemplateLoader} * for the Spring-provided macros, added to the end of the list. */ @Override @@ -117,7 +120,7 @@ public class FreeMarkerConfigurer extends FreeMarkerConfigurationFactory /** - * Return the Configuration object wrapped by this bean. + * Return the {@link Configuration} object wrapped by this bean. */ @Override public Configuration getConfiguration() { 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 d82d0bebb7f..e513b5b2e71 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -46,27 +46,40 @@ import org.springframework.web.servlet.view.AbstractTemplateView; /** * View using the FreeMarker template engine. * - *
Exposes the following JavaBean properties: + *
Exposes the following configuration properties: *
Depends on a single {@link FreeMarkerConfig} object such as {@link FreeMarkerConfigurer} - * being accessible in the current web application context, with any bean name. - * Alternatively, you can set the FreeMarker {@link Configuration} object as a - * bean property. See {@link #setConfiguration} for more details on the impacts - * of this approach. + *
Depends on a single {@link FreeMarkerConfig} object such as + * {@link FreeMarkerConfigurer} being accessible in the current web application + * context. Alternatively the FreeMarker {@link Configuration} can be set directly + * via {@link #setConfiguration}. * - *
Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher. + *
Note: To ensure that the correct encoding is used when rendering the + * response, set the {@linkplain #setContentType(String) content type} with an + * appropriate {@code charset} attribute — for example, + * {@code "text/html;charset=UTF-8"}. When using {@link FreeMarkerViewResolver} + * to create the view for you, set the + * {@linkplain FreeMarkerViewResolver#setContentType(String) content type} + * directly in the {@code FreeMarkerViewResolver}. + * + *
Note: Spring's FreeMarker support requires FreeMarker 2.3.21 or higher. * As of Spring Framework 6.0, FreeMarker templates are rendered in a minimal * fashion without JSP support, just exposing request attributes in addition * to the MVC-provided model map for alignment with common Servlet resources. * * @author Darren Davison * @author Juergen Hoeller + * @author Sam Brannen * @since 03.03.2004 * @see #setUrl * @see #setExposeSpringMacroHelpers @@ -85,17 +98,36 @@ public class FreeMarkerView extends AbstractTemplateView { /** - * Set the encoding of the FreeMarker template file. Default is determined - * by the FreeMarker Configuration: "ISO-8859-1" if not specified otherwise. - *
Specify the encoding in the FreeMarker Configuration rather than per - * template if all your templates share a common encoding. + * Set the encoding used to decode byte sequences to character sequences when + * reading the FreeMarker template file for this view. + *
Defaults to {@code null} to signal that the FreeMarker + * {@link Configuration} should be used to determine the encoding. + *
A non-null encoding will override the default encoding determined by + * the FreeMarker {@code Configuration}. + *
If the encoding is not explicitly set here or in the FreeMarker + * {@code Configuration}, FreeMarker will read template files using the platform + * file encoding (defined by the JVM system property {@code file.encoding}) + * or {@code "utf-8"} if the platform file encoding is undefined. + *
It's recommended to specify the encoding in the FreeMarker {@code Configuration} + * rather than per template if all your templates share a common encoding. + *
Note that the specified or default encoding is not used for template + * rendering. Instead, an explicit encoding must be specified for the rendering + * process. See the note in the {@linkplain FreeMarkerView class-level + * documentation} for details. + * @see freemarker.template.Configuration#setDefaultEncoding + * @see #getEncoding() + * @see #setContentType(String) */ public void setEncoding(@Nullable String encoding) { this.encoding = encoding; } /** - * Return the encoding for the FreeMarker template. + * Get the encoding used to decode byte sequences to character sequences + * when reading the FreeMarker template file for this view, or {@code null} + * to signal that the FreeMarker {@link Configuration} should be used to + * determine the encoding. + * @see #setEncoding(String) */ @Nullable protected String getEncoding() { @@ -103,8 +135,8 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Set the FreeMarker Configuration to be used by this view. - *
If this is not set, the default lookup will occur: a single {@link FreeMarkerConfig} + * Set the FreeMarker {@link Configuration} to be used by this view. + *
If not set, the default lookup will occur: a single {@link FreeMarkerConfig} * is expected in the current web application context, with any bean name. */ public void setConfiguration(@Nullable Configuration configuration) { @@ -112,7 +144,7 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Return the FreeMarker configuration used by this view. + * Return the FreeMarker {@link Configuration} used by this view. */ @Nullable protected Configuration getConfiguration() { @@ -120,7 +152,7 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Obtain the FreeMarker configuration for actual use. + * Obtain the FreeMarker {@link Configuration} for actual use. * @return the FreeMarker configuration (never {@code null}) * @throws IllegalStateException in case of no Configuration object set * @since 5.0 @@ -133,8 +165,8 @@ public class FreeMarkerView extends AbstractTemplateView { /** - * Invoked on startup. Looks for a single FreeMarkerConfig bean to - * find the relevant Configuration for this factory. + * Invoked on startup. Looks for a single {@link FreeMarkerConfig} bean to + * find the relevant {@link Configuration} for this view. *
Checks that the template for the default Locale can be found: * FreeMarker will check non-Locale-specific templates if a * locale-specific one is not found. @@ -149,9 +181,9 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext. - * @return the Configuration instance to use for FreeMarkerViews - * @throws BeansException if no Configuration instance could be found + * Autodetect a {@link FreeMarkerConfig} object via the {@code ApplicationContext}. + * @return the {@code FreeMarkerConfig} instance to use for FreeMarkerViews + * @throws BeansException if no {@link FreeMarkerConfig} bean could be found * @see #getApplicationContext * @see #setConfiguration */ @@ -170,7 +202,7 @@ public class FreeMarkerView extends AbstractTemplateView { /** * Return the configured FreeMarker {@link ObjectWrapper}, or the - * {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified. + * {@linkplain ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified. * @see freemarker.template.Configuration#getObjectWrapper() */ protected ObjectWrapper getObjectWrapper() { @@ -209,7 +241,7 @@ public class FreeMarkerView extends AbstractTemplateView { /** * Process the model map by merging it with the FreeMarker template. - * Output is directed to the servlet response. + *
Output is directed to the servlet response. *
This method can be overridden if custom behavior is needed. */ @Override @@ -284,12 +316,12 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Retrieve the FreeMarker template for the given locale, - * to be rendering by this view. + * Retrieve the FreeMarker {@link Template} for the given locale, to be + * rendered by this view. *
By default, the template specified by the "url" bean property * will be retrieved. * @param locale the current locale - * @return the FreeMarker template to render + * @return the FreeMarker {@code Template} to render * @throws IOException if the template file could not be retrieved * @see #setUrl * @see #getTemplate(String, java.util.Locale) @@ -301,14 +333,15 @@ public class FreeMarkerView extends AbstractTemplateView { } /** - * Retrieve the FreeMarker template specified by the given name, - * using the encoding specified by the "encoding" bean property. + * Retrieve the FreeMarker {@link Template} for the specified name and locale, + * using the {@linkplain #setEncoding(String) configured encoding} if set. *
Can be called by subclasses to retrieve a specific template, * for example to render multiple templates into a single view. * @param name the file name of the desired template * @param locale the current locale * @return the FreeMarker template * @throws IOException if the template file could not be retrieved + * @see #setEncoding(String) */ protected Template getTemplate(String name, Locale locale) throws IOException { return (getEncoding() != null ? diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java index b3fd7b7ffdc..17c728dfe6a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 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. @@ -24,17 +24,24 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView; * that supports {@link FreeMarkerView} (i.e. FreeMarker templates) and custom subclasses of it. * *
The view class for all views generated by this resolver can be specified - * via the "viewClass" property. See UrlBasedViewResolver's javadoc for details. + * via the "viewClass" property. See {@code UrlBasedViewResolver} for details. * - *
Note: When chaining ViewResolvers, a FreeMarkerViewResolver will + *
Note: To ensure that the correct encoding is used when the rendering + * the response, set the {@linkplain #setContentType(String) content type} with an + * appropriate {@code charset} attribute — for example, + * {@code "text/html;charset=UTF-8"}. + * + *
Note: When chaining ViewResolvers, a {@code FreeMarkerViewResolver} will * check for the existence of the specified template resources and only return - * a non-null View object if the template was actually found. + * a non-null {@code View} object if the template was actually found. * * @author Juergen Hoeller + * @author Sam Brannen * @since 1.1 * @see #setViewClass * @see #setPrefix * @see #setSuffix + * @see #setContentType * @see #setRequestContextAttribute * @see #setExposeSpringMacroHelpers * @see FreeMarkerView 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 168552f0f38..573a7cd5a24 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 @@ -16,9 +16,10 @@ package org.springframework.web.servlet.config.annotation; -import java.io.IOException; +import java.nio.charset.StandardCharsets; -import jakarta.servlet.ServletException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; @@ -43,47 +44,197 @@ import static org.assertj.core.api.Assertions.assertThatRuntimeException; * Integration tests for view resolution with {@code @EnableWebMvc}. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.1 */ class ViewResolutionIntegrationTests { - private static final String EXPECTED_BODY = "
Hello World!"; + private static final String EXPECTED_BODY = "Hello, Java Café"; - @Test - void freemarker() throws Exception { - MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + @BeforeAll + static void verifyDefaultFileEncoding() { + assertThat(System.getProperty("file.encoding")).as("JVM default file encoding").isEqualTo("UTF-8"); } - @Test // SPR-12013 - void freemarkerWithExistingViewResolver() throws Exception { - MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); - } - @Test - void groovyMarkup() throws Exception { - MockHttpServletResponse response = runTest(GroovyMarkupWebConfig.class); - assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); - } + @Nested + class FreeMarkerTests { + + @Test + void freemarkerWithInvalidConfig() { + assertThatRuntimeException() + .isThrownBy(() -> runTest(InvalidFreeMarkerWebConfig.class)) + .withMessageContaining("In addition to a FreeMarker view resolver "); + } + + @Test + void freemarkerWithDefaults() throws Exception { + MockHttpServletResponse response = runTest(FreeMarkerWebConfig.class); + assertThat(response.isCharset()).as("character encoding set in response").isTrue(); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + // 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"); + } + + @Test // gh-16629, gh-33071 + void freemarkerWithExistingViewResolver() throws Exception { + MockHttpServletResponse response = runTest(ExistingViewResolverConfig.class); + assertThat(response.isCharset()).as("character encoding set in response").isTrue(); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + // 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"); + } + + @Test // gh-33071 + void freemarkerWithExplicitDefaultEncoding() throws Exception { + MockHttpServletResponse response = runTest(ExplicitDefaultEncodingConfig.class); + assertThat(response.isCharset()).as("character encoding set in response").isTrue(); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + // 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"); + } + + @Test // gh-33071 + void freemarkerWithExplicitDefaultEncodingAndContentType() throws Exception { + MockHttpServletResponse response = runTest(ExplicitDefaultEncodingAndContentTypeConfig.class); + assertThat(response.isCharset()).as("character encoding set in response").isTrue(); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + // 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"); + } + + + @Configuration + static class InvalidFreeMarkerWebConfig extends WebMvcConfigurationSupport { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + } + + @Configuration + static class FreeMarkerWebConfig extends AbstractWebConfig { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } - @Test - void freemarkerInvalidConfig() { - assertThatRuntimeException() - .isThrownBy(() -> runTest(InvalidFreeMarkerWebConfig.class)) - .withMessageContaining("In addition to a FreeMarker view resolver "); + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/"); + return configurer; + } + } + + @Configuration + static class ExistingViewResolverConfig extends AbstractWebConfig { + + @Bean + public FreeMarkerViewResolver freeMarkerViewResolver() { + return new FreeMarkerViewResolver("", ".ftl"); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/"); + return configurer; + } + } + + @Configuration + static class ExplicitDefaultEncodingConfig extends AbstractWebConfig { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/"); + configurer.setDefaultEncoding(StandardCharsets.UTF_8.name()); + return configurer; + } + } + + @Configuration + static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebConfig { + + @Bean + public FreeMarkerViewResolver freeMarkerViewResolver() { + FreeMarkerViewResolver resolver = new FreeMarkerViewResolver("", ".ftl"); + resolver.setContentType("text/html;charset=UTF-16"); + return resolver; + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/"); + configurer.setDefaultEncoding(StandardCharsets.UTF_8.name()); + return configurer; + } + } } - @Test - void groovyMarkupInvalidConfig() { - assertThatRuntimeException() - .isThrownBy(() -> runTest(InvalidGroovyMarkupWebConfig.class)) - .withMessageContaining("In addition to a Groovy markup view resolver "); + @Nested + class GroovyMarkupTests { + + @Test + void groovyMarkupInvalidConfig() { + assertThatRuntimeException() + .isThrownBy(() -> runTest(InvalidGroovyMarkupWebConfig.class)) + .withMessageContaining("In addition to a Groovy markup view resolver "); + } + + @Test + void groovyMarkup() throws Exception { + MockHttpServletResponse response = runTest(GroovyMarkupWebConfig.class); + assertThat(response.getContentAsString()).isEqualTo(EXPECTED_BODY); + } + + + @Configuration + static class InvalidGroovyMarkupWebConfig extends WebMvcConfigurationSupport { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.groovy(); + } + } + + @Configuration + static class GroovyMarkupWebConfig extends AbstractWebConfig { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.groovy(); + } + + @Bean + public GroovyMarkupConfigurer groovyMarkupConfigurer() { + GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); + configurer.setResourceLoaderPath("/WEB-INF/"); + return configurer; + } + } } - private MockHttpServletResponse runTest(Class> configClass) throws ServletException, IOException { + private static MockHttpServletResponse runTest(Class> configClass) throws Exception { String basePath = "org/springframework/web/servlet/config/annotation"; MockServletContext servletContext = new MockServletContext(basePath); MockServletConfig servletConfig = new MockServletConfig(servletContext); @@ -105,8 +256,8 @@ class ViewResolutionIntegrationTests { static class SampleController { @GetMapping - public String sample(ModelMap model) { - model.addAttribute("hello", "Hello World!"); + String index(ModelMap model) { + model.put("hello", "Hello"); return "index"; } } @@ -120,73 +271,4 @@ class ViewResolutionIntegrationTests { } } - @Configuration - static class FreeMarkerWebConfig extends AbstractWebConfig { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.freeMarker(); - } - - @Bean - public FreeMarkerConfigurer freeMarkerConfigurer() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setTemplateLoaderPath("/WEB-INF/"); - return configurer; - } - } - - /** - * Test @EnableWebMvc in the presence of a pre-existing ViewResolver. - */ - @Configuration - static class ExistingViewResolverConfig extends AbstractWebConfig { - - @Bean - public FreeMarkerViewResolver freeMarkerViewResolver() { - return new FreeMarkerViewResolver("", ".ftl"); - } - - @Bean - public FreeMarkerConfigurer freeMarkerConfigurer() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setTemplateLoaderPath("/WEB-INF/"); - return configurer; - } - } - - @Configuration - static class GroovyMarkupWebConfig extends AbstractWebConfig { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.groovy(); - } - - @Bean - public GroovyMarkupConfigurer groovyMarkupConfigurer() { - GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); - configurer.setResourceLoaderPath("/WEB-INF/"); - return configurer; - } - } - - @Configuration - static class InvalidFreeMarkerWebConfig extends WebMvcConfigurationSupport { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.freeMarker(); - } - } - - @Configuration - static class InvalidGroovyMarkupWebConfig extends WebMvcConfigurationSupport { - - @Override - public void configureViewResolvers(ViewResolverRegistry registry) { - registry.groovy(); - } - } - } 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 f9ad1fdc6ec..84bf3b81e10 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 @@ -${hello} \ No newline at end of file +${hello}, Java Café \ No newline at end of file diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.tpl b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.tpl index 70708a00bc4..ab7967985bf 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.tpl +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/annotation/WEB-INF/index.tpl @@ -1 +1 @@ -html { body(hello) } \ No newline at end of file +html { body(hello + ", Java Café") } \ No newline at end of file