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