From 5d6e143ff4ec3e10a4b8755cfa971b382fcaaddb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:32:14 +0200 Subject: [PATCH 1/2] Remove invalid configuration in RequestMappingViewResolutionIntegrationTests Prior to this commit, RequestMappingViewResolutionIntegrationTests invoked the following: configurer.setTemplateLoaderPath( "classpath*:org/springframework/web/reactive/view/freemarker/"); However, that configuration is invalid since `classpath*:` is not supported for a `templateLoaderPath`. Despite that, the tests still passed since FreeMarkerConfigurer already registers a new ClassTemplateLoader(FreeMarkerConfigurer.class, ""), which automatically finds template files in the same package as FreeMarkerConfigurer (for the "spring.ftl" macro library support) and coincidentally RequestMappingViewResolutionIntegrationTests as well (which resides in the same package). This commit therefore removes the invalid configuration and adds a comment to explain what's going on. --- .../RequestMappingViewResolutionIntegrationTests.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java index eb0b9e4c1b9..266e492c6cb 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingViewResolutionIntegrationTests.java @@ -111,10 +111,11 @@ class RequestMappingViewResolutionIntegrationTests extends AbstractRequestMappin @Bean public FreeMarkerConfigurer freeMarkerConfig() { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer.setPreferFileSystemAccess(false); - configurer.setTemplateLoaderPath("classpath*:org/springframework/web/reactive/view/freemarker/"); - return configurer; + // No need to configure a custom template loader path via setTemplateLoaderPath(), + // since FreeMarkerConfigurer already registers a + // new ClassTemplateLoader(FreeMarkerConfigurer.class, ""), which automatically + // finds template files in the same package as this test class. + return new FreeMarkerConfigurer(); } } 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 2/2] 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