From 02af3e7a17db9f63ced52425d06558eb50deb495 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:03:14 +0100 Subject: [PATCH] Introduce setDefaultCharset() in AbstractResourceBasedMessageSource Traditionally, AbstractResourceBasedMessageSource has only had a setDefaultEncoding() method which accepts the name of the default encoding character set. However, although we have recently made a concerted effort within the framework to introduce support for supplying a Charset instead of a character set's name, we had overlooked this particular scenario. In light of that, this commit introduces setDefaultCharset(Charset) and getDefaultCharset() methods in AbstractResourceBasedMessageSource and makes direct use of the available Charset in ReloadableResourceBundleMessageSource and ResourceBundleMessageSource. Furthermore, although technically a regression in behavior, invoking setDefaultEncoding() on such MessageSource implementations with an invalid character set name now results in an immediate UnsupportedCharsetException at configuration time instead of a NoSuchMessageException at runtime, which will help users to more easily detect misconfiguration. Closes gh-36413 --- .../AbstractResourceBasedMessageSource.java | 37 +++++++++++++++++-- ...ReloadableResourceBundleMessageSource.java | 29 +++++++++------ .../support/ResourceBundleMessageSource.java | 33 +++++++++-------- .../ResourceBundleMessageSourceTests.java | 30 ++++++++++----- .../util/DefaultPropertiesPersister.java | 2 +- 5 files changed, 89 insertions(+), 42 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java index b5cc7a03674..d606231a131 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractResourceBasedMessageSource.java @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.nio.charset.Charset; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; @@ -32,6 +33,7 @@ import org.springframework.util.ObjectUtils; * configuration methods and corresponding semantic definitions. * * @author Juergen Hoeller + * @author Sam Brannen * @since 4.3 * @see ResourceBundleMessageSource * @see ReloadableResourceBundleMessageSource @@ -40,7 +42,7 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage private final Set basenameSet = new LinkedHashSet<>(4); - private @Nullable String defaultEncoding; + private @Nullable Charset defaultCharset; private boolean fallbackToSystemLocale = true; @@ -118,25 +120,52 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage /** * Set the default charset to use for parsing properties files. - * Used if no file-specific charset is specified for a file. + *

Used if no file-specific charset is specified for a file. *

The effective default is the {@code java.util.Properties} * default encoding: ISO-8859-1. A {@code null} value indicates * the platform default encoding. *

Only applies to classic properties files, not to XML files. * @param defaultEncoding the default charset + * @see #setDefaultCharset(Charset) */ public void setDefaultEncoding(@Nullable String defaultEncoding) { - this.defaultEncoding = defaultEncoding; + this.defaultCharset = (defaultEncoding != null ? Charset.forName(defaultEncoding) : null); } /** * Return the default charset to use for parsing properties files, if any. * @since 4.3 + * @see #getDefaultCharset() */ protected @Nullable String getDefaultEncoding() { - return this.defaultEncoding; + return (this.defaultCharset != null ? this.defaultCharset.name() : null); } + /** + * Set the default {@link Charset} to use for parsing properties files. + *

Used if no file-specific charset is specified for a file. + *

The effective default is the {@code java.util.Properties} + * default encoding: ISO-8859-1. A {@code null} value indicates + * the platform default encoding. + *

Only applies to classic properties files, not to XML files. + * @param defaultCharset the default charset + * @since 7.0.6 + * @see #setDefaultEncoding(String) + */ + public void setDefaultCharset(@Nullable Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Return the default charset to use for parsing properties files, if any. + * @since 7.0.6 + * @see #setDefaultCharset(Charset) + */ + protected @Nullable Charset getDefaultCharset() { + return this.defaultCharset; + } + + /** * Set whether to fall back to the system Locale if no files for a specific * Locale have been found. Default is "true"; if this is turned off, the only diff --git a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 03d4d296789..4cfd9a03882 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -19,6 +19,7 @@ package org.springframework.context.support; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.Charset; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -80,9 +81,10 @@ import org.springframework.util.StringUtils; * * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Sam Brannen * @see #setCacheSeconds * @see #setBasenames - * @see #setDefaultEncoding + * @see #setDefaultCharset * @see #setFileEncodings * @see #setPropertiesPersister * @see #setResourceLoader @@ -135,7 +137,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased /** * Set per-file charsets to use for parsing properties files. *

Only applies to classic properties files, not to XML files. - * @param fileEncodings a Properties with filenames as keys and charset + * @param fileEncodings a Properties object with filenames as keys and charset * names as values. Filenames have to match the basename syntax, * with optional locale-specific components: for example, "WEB-INF/messages" * or "WEB-INF/messages_en". @@ -567,18 +569,21 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased this.propertiesPersister.loadFromXml(props, is); } else { - String encoding = null; + Charset charset = null; if (this.fileEncodings != null) { - encoding = this.fileEncodings.getProperty(filename); - } - if (encoding == null) { - encoding = getDefaultEncoding(); - } - if (encoding != null) { - if (logger.isDebugEnabled()) { - logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); + String charsetName = this.fileEncodings.getProperty(filename); + if (charsetName != null) { + charset = Charset.forName(charsetName); } - this.propertiesPersister.load(props, new InputStreamReader(is, encoding)); + } + if (charset == null) { + charset = getDefaultCharset(); + } + if (charset != null) { + if (logger.isDebugEnabled()) { + logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + charset + "'"); + } + this.propertiesPersister.load(props, new InputStreamReader(is, charset)); } else { if (logger.isDebugEnabled()) { diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java index 0827969b4ff..954d6edb780 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -22,6 +22,8 @@ import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.net.URLConnection; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.Locale; import java.util.Map; @@ -57,11 +59,11 @@ import org.springframework.util.ClassUtils; * This means that "test.theme" is effectively equivalent to "test/theme". * *

On the classpath, bundle resources will be read with the locally configured - * {@link #setDefaultEncoding encoding}: by default, ISO-8859-1; consider switching + * {@link #setDefaultCharset Charset}: by default, ISO-8859-1; consider switching * this to UTF-8, or to {@code null} for the platform default encoding. On the JDK 9+ * module path where locally provided {@code ResourceBundle.Control} handles are not * supported, this MessageSource always falls back to {@link ResourceBundle#getBundle} - * retrieval with the platform default encoding: UTF-8 with a ISO-8859-1 fallback on + * retrieval with the platform default encoding: UTF-8 with an ISO-8859-1 fallback on * JDK 9+ (configurable through the "java.util.PropertyResourceBundle.encoding" system * property). Note that {@link #loadBundle(Reader)}/{@link #loadBundle(InputStream)} * won't be called in this case either, effectively ignoring overrides in subclasses. @@ -70,6 +72,7 @@ import org.springframework.util.ClassUtils; * @author Rod Johnson * @author Juergen Hoeller * @author Qimiao Chen + * @author Sam Brannen * @see #setBasenames * @see ReloadableResourceBundleMessageSource * @see java.util.ResourceBundle @@ -83,7 +86,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou /** * Cache to hold loaded ResourceBundles. - * This Map is keyed with the bundle basename, which holds a Map that is + *

This Map is keyed with the bundle basename, which holds a Map that is * keyed with the Locale and in turn holds the ResourceBundle instances. * This allows for very efficient hash lookups, significantly faster * than the ResourceBundle class's own cache. @@ -93,7 +96,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou /** * Cache to hold already generated MessageFormats. - * This Map is keyed with the ResourceBundle, which holds a Map that is + *

This Map is keyed with the ResourceBundle, which holds a Map that is * keyed with the message code, which in turn holds a Map that is keyed * with the Locale and holds the MessageFormat values. This allows for * very efficient hash lookups without concatenated keys. @@ -106,7 +109,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou public ResourceBundleMessageSource() { - setDefaultEncoding("ISO-8859-1"); + setDefaultCharset(StandardCharsets.ISO_8859_1); } @@ -239,12 +242,12 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou catch (UnsupportedOperationException ex) { // Probably in a Java Module System environment on JDK 9+ this.control = null; - String encoding = getDefaultEncoding(); - if (encoding != null && logger.isInfoEnabled()) { + Charset charset = getDefaultCharset(); + if (charset != null && logger.isInfoEnabled()) { logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" + - encoding + "' but ResourceBundle.Control is not supported in current system environment: " + + charset + "' but ResourceBundle.Control is not supported in current system environment: " + ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + - "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + + "platform default encoding. Consider setting the 'defaultCharset' property to 'null' " + "for participating in the platform default and therefore avoiding this log message."); } } @@ -256,7 +259,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou /** * Load a property-based resource bundle from the given reader. - *

This will be called in case of a {@link #setDefaultEncoding "defaultEncoding"}, + *

This will be called in case of a {@linkplain #setDefaultCharset "defaultCharset"}, * including {@link ResourceBundleMessageSource}'s default ISO-8859-1 encoding. * Note that this method can only be called with a {@code ResourceBundle.Control}: * When running on the JDK 9+ module path where such control handles are not @@ -276,9 +279,9 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou /** * Load a property-based resource bundle from the given input stream, * picking up the default properties encoding on JDK 9+. - *

This will only be called with {@link #setDefaultEncoding "defaultEncoding"} + *

This will only be called with {@linkplain #setDefaultCharset "defaultCharset"} * set to {@code null}, explicitly enforcing the platform default encoding - * (which is UTF-8 with a ISO-8859-1 fallback on JDK 9+ but configurable + * (which is UTF-8 with an ISO-8859-1 fallback on JDK 9+ but configurable * through the "java.util.PropertyResourceBundle.encoding" system property). * Note that this method can only be called with a {@code ResourceBundle.Control}: * When running on the JDK 9+ module path where such control handles are not @@ -404,9 +407,9 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou inputStream = classLoader.getResourceAsStream(resourceName); } if (inputStream != null) { - String encoding = getDefaultEncoding(); - if (encoding != null) { - try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) { + Charset charset = getDefaultCharset(); + if (charset != null) { + try (InputStreamReader bundleReader = new InputStreamReader(inputStream, charset)) { return loadBundle(bundleReader); } } diff --git a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java index edeef0427ac..ad20120c48f 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ResourceBundleMessageSourceTests.java @@ -16,6 +16,7 @@ package org.springframework.context.support; +import java.nio.charset.UnsupportedCharsetException; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -30,14 +31,18 @@ import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.context.i18n.LocaleContextHolder; +import static java.nio.charset.StandardCharsets.ISO_8859_1; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** + * Tests for {@link ResourceBundleMessageSource} and {@link ReloadableResourceBundleMessageSource}. + * * @author Juergen Hoeller * @author Sebastien Deleuze + * @author Sam Brannen * @since 03.02.2004 */ class ResourceBundleMessageSourceTests { @@ -272,23 +277,28 @@ class ResourceBundleMessageSourceTests { assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); } + @Test // gh-36413 + void resourceBundleMessageSourceWithInvalidDefaultCharsetName() { + ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); + assertThatExceptionOfType(UnsupportedCharsetException.class).isThrownBy(() -> ms.setDefaultEncoding("BOGUS")); + } + @Test - void resourceBundleMessageSourceWithDefaultCharset() { + void resourceBundleMessageSourceWithDefaultCharsetName() { ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); ms.setBasename("org/springframework/context/support/messages"); - ms.setDefaultEncoding("ISO-8859-1"); + ms.setDefaultEncoding(ISO_8859_1.name()); assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); } - @Test - void resourceBundleMessageSourceWithInappropriateDefaultCharset() { + @Test // gh-36413 + void resourceBundleMessageSourceWithDefaultCharset() { ResourceBundleMessageSource ms = new ResourceBundleMessageSource(); ms.setBasename("org/springframework/context/support/messages"); - ms.setDefaultEncoding("argh"); - ms.setFallbackToSystemLocale(false); - assertThatExceptionOfType(NoSuchMessageException.class).isThrownBy(() -> - ms.getMessage("code1", null, Locale.ENGLISH)); + ms.setDefaultCharset(ISO_8859_1); + assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); + assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); } @Test @@ -353,13 +363,13 @@ class ResourceBundleMessageSourceTests { void reloadableResourceBundleMessageSourceWithDefaultCharset() { ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); ms.setBasename("org/springframework/context/support/messages"); - ms.setDefaultEncoding("ISO-8859-1"); + ms.setDefaultCharset(ISO_8859_1); assertThat(ms.getMessage("code1", null, Locale.ENGLISH)).isEqualTo("message1"); assertThat(ms.getMessage("code2", null, Locale.GERMAN)).isEqualTo("nachricht2"); } @Test - void reloadableResourceBundleMessageSourceWithInappropriateDefaultCharset() { + void reloadableResourceBundleMessageSourceWithInappropriateDefaultCharsetName() { ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource(); ms.setBasename("org/springframework/context/support/messages"); ms.setDefaultEncoding("unicode"); diff --git a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java index d1aef82e09c..06831fcc2f8 100644 --- a/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java +++ b/spring-core/src/main/java/org/springframework/util/DefaultPropertiesPersister.java @@ -43,7 +43,7 @@ import java.util.Properties; * should already apply proper decoding/encoding of characters. If you prefer * to escape unicode characters in your properties files, do not specify * an encoding for a Reader/Writer (like ReloadableResourceBundleMessageSource's - * "defaultEncoding" and "fileEncodings" properties). + * "defaultCharset" and "fileEncodings" properties). * * @author Juergen Hoeller * @author Sebastien Deleuze