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 da8af17fb0..44d284c9a5 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
@@ -37,12 +37,16 @@ import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;
/**
- * {@link org.springframework.context.MessageSource} implementation that
- * accesses resource bundles using specified basenames. This class uses
- * {@link java.util.Properties} instances as its custom data structure for
- * messages, loading them via a {@link org.springframework.util.PropertiesPersister}
- * strategy: The default strategy is capable of loading properties files
- * with a specific character encoding, if desired.
+ * Spring-specific {@link org.springframework.context.MessageSource} implementation
+ * that accesses resource bundles using specified basenames, participating in the
+ * Spring {@link org.springframework.context.ApplicationContext}'s resource loading.
+ *
+ *
In contrast to the JDK-based {@link ResourceBundleMessageSource}, this class uses
+ * {@link java.util.Properties} instances as its custom data structure for messages,
+ * loading them via a {@link org.springframework.util.PropertiesPersister} strategy
+ * from Spring {@link Resource} handles. This strategy is not only capable of
+ * reloading files based on timestamp changes, but also of loading properties files
+ * with a specific character encoding. It will detect XML property files as well.
*
*
In contrast to {@link ResourceBundleMessageSource}, this class supports
* reloading of properties files through the {@link #setCacheSeconds "cacheSeconds"}
@@ -171,7 +175,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
* Set the default charset to use for parsing properties files.
* Used if no file-specific charset is specified for a file.
*
Default is none, using the java.util.Properties
- * default encoding.
+ * default encoding: ISO-8859-1.
*
Only applies to classic properties files, not to XML files.
* @param defaultEncoding the default charset
* @see #setFileEncodings
@@ -201,10 +205,9 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
* fallback will be the default file (e.g. "messages.properties" for
* basename "messages").
*
Falling back to the system Locale is the default behavior of
- * java.util.ResourceBundle. However, this is often not
- * desirable in an application server environment, where the system Locale
- * is not relevant to the application at all: Set this flag to "false"
- * in such a scenario.
+ * java.util.ResourceBundle. However, this is often not desirable
+ * in an application server environment, where the system Locale is not relevant
+ * to the application at all: Set this flag to "false" in such a scenario.
*/
public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
this.fallbackToSystemLocale = fallbackToSystemLocale;
@@ -448,7 +451,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractMessageSource
* @param propHolder the current PropertiesHolder for the bundle
*/
protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
- long refreshTimestamp = (this.cacheMillis < 0) ? -1 : System.currentTimeMillis();
+ long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System.currentTimeMillis());
Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if (!resource.exists()) {
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 a3426dc1d5..bf99812553 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
@@ -16,14 +16,24 @@
package org.springframework.context.support;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
+import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import org.springframework.beans.factory.BeanClassLoaderAware;
+import org.springframework.core.JdkVersion;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -58,6 +68,12 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
private String[] basenames = new String[0];
+ private String defaultEncoding;
+
+ private boolean fallbackToSystemLocale = true;
+
+ private long cacheMillis = -1;
+
private ClassLoader bundleClassLoader;
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
@@ -133,6 +149,59 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
}
}
+ /**
+ * Set the default charset to use for parsing resource bundle files.
+ *
Default is none, using the java.util.ResourceBundle
+ * default encoding: ISO-8859-1.
+ *
NOTE: Only works on JDK 1.6 and higher. Consider using
+ * {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support
+ * and more flexibility in setting of an encoding per file.
+ */
+ public void setDefaultEncoding(String defaultEncoding) {
+ this.defaultEncoding = defaultEncoding;
+ }
+
+ /**
+ * 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
+ * fallback will be the default file (e.g. "messages.properties" for
+ * basename "messages").
+ *
Falling back to the system Locale is the default behavior of
+ * java.util.ResourceBundle. However, this is often not desirable
+ * in an application server environment, where the system Locale is not relevant
+ * to the application at all: Set this flag to "false" in such a scenario.
+ *
NOTE: Only works on JDK 1.6 and higher. Consider using
+ * {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support.
+ */
+ public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
+ this.fallbackToSystemLocale = fallbackToSystemLocale;
+ }
+
+ /**
+ * Set the number of seconds to cache loaded resource bundle files.
+ *
+ * - Default is "-1", indicating to cache forever.
+ *
- A positive number will expire resource bundles after the given
+ * number of seconds. This is essentially the interval between refresh checks.
+ * Note that a refresh attempt will first check the last-modified timestamp
+ * of the file before actually reloading it; so if files don't change, this
+ * interval can be set rather low, as refresh attempts will not actually reload.
+ *
- A value of "0" will check the last-modified timestamp of the file on
+ * every message access. Do not use this in a production environment!
+ *
- Note that depending on your ClassLoader, expiration might not work reliably
+ * since the ClassLoader may hold on to a cached version of the bundle file.
+ * Consider {@link ReloadableResourceBundleMessageSource} in combination
+ * with resource bundle files in a non-classpath location.
+ *
+ * NOTE: Only works on JDK 1.6 and higher. Consider using
+ * {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support
+ * and more flexibility in terms of the kinds of resources to load from
+ * (in particular from outside of the classpath where expiration works reliably).
+ */
+ public void setCacheSeconds(int cacheSeconds) {
+ this.cacheMillis = (cacheSeconds * 1000);
+ }
+
/**
* Set the ClassLoader to load resource bundles with.
*
Default is the containing BeanFactory's
@@ -201,30 +270,38 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
* found for the given basename and Locale
*/
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
- synchronized (this.cachedResourceBundles) {
- Map localeMap = this.cachedResourceBundles.get(basename);
- if (localeMap != null) {
- ResourceBundle bundle = localeMap.get(locale);
- if (bundle != null) {
+ if (this.cacheMillis >= 0) {
+ // Fresh ResourceBundle.getBundle call in order to let ResourceBundle
+ // do its native caching, at the expense of more extensive lookup steps.
+ return doGetBundle(basename, locale);
+ }
+ else {
+ // Cache forever: prefer locale cache over repeated getBundle calls.
+ synchronized (this.cachedResourceBundles) {
+ Map localeMap = this.cachedResourceBundles.get(basename);
+ if (localeMap != null) {
+ ResourceBundle bundle = localeMap.get(locale);
+ if (bundle != null) {
+ return bundle;
+ }
+ }
+ try {
+ ResourceBundle bundle = doGetBundle(basename, locale);
+ if (localeMap == null) {
+ localeMap = new HashMap();
+ this.cachedResourceBundles.put(basename, localeMap);
+ }
+ localeMap.put(locale, bundle);
return bundle;
}
- }
- try {
- ResourceBundle bundle = doGetBundle(basename, locale);
- if (localeMap == null) {
- localeMap = new HashMap();
- this.cachedResourceBundles.put(basename, localeMap);
+ catch (MissingResourceException ex) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
+ }
+ // Assume bundle not found
+ // -> do NOT throw the exception to allow for checking parent message source.
+ return null;
}
- localeMap.put(locale, bundle);
- return bundle;
- }
- catch (MissingResourceException ex) {
- if (logger.isWarnEnabled()) {
- logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
- }
- // Assume bundle not found
- // -> do NOT throw the exception to allow for checking parent message source.
- return null;
}
}
}
@@ -239,7 +316,20 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
* @see #getBundleClassLoader()
*/
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
- return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
+ if ((this.defaultEncoding != null && !"ISO-8859-1".equals(this.defaultEncoding)) ||
+ !this.fallbackToSystemLocale || this.cacheMillis >= 0) {
+ // Custom Control required...
+ if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {
+ throw new IllegalStateException("Cannot use 'defaultEncoding', 'fallbackToSystemLocale' and " +
+ "'cacheSeconds' on the standard ResourceBundleMessageSource when running on Java 5. " +
+ "Consider using ReloadableResourceBundleMessageSource instead.");
+ }
+ return new ControlBasedResourceBundleFactory().getBundle(basename, locale);
+ }
+ else {
+ // Good old standard call...
+ return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
+ }
}
/**
@@ -298,7 +388,6 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
}
}
-
/**
* Show the configuration of this MessageSource.
*/
@@ -308,4 +397,101 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";
}
+
+ /**
+ * Factory indirection for runtime isolation of the optional dependencv on
+ * Java 6's Control class.
+ * @see ResourceBundle#getBundle(String, java.util.Locale, ClassLoader, java.util.ResourceBundle.Control)
+ * @see MessageSourceControl
+ */
+ private class ControlBasedResourceBundleFactory {
+
+ public ResourceBundle getBundle(String basename, Locale locale) {
+ return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), new MessageSourceControl());
+ }
+ }
+
+
+ /**
+ * Custom implementation of Java 6's ResourceBundle.Control,
+ * adding support for custom file encodings, deactivating the fallback to the
+ * system locale and activating ResourceBundle's native cache, if desired.
+ */
+ private class MessageSourceControl extends ResourceBundle.Control {
+
+ @Override
+ public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
+ throws IllegalAccessException, InstantiationException, IOException {
+ if (format.equals("java.properties")) {
+ String bundleName = toBundleName(baseName, locale);
+ final String resourceName = toResourceName(bundleName, "properties");
+ final ClassLoader classLoader = loader;
+ final boolean reloadFlag = reload;
+ InputStream stream;
+ try {
+ stream = AccessController.doPrivileged(
+ new PrivilegedExceptionAction() {
+ public InputStream run() throws IOException {
+ InputStream is = null;
+ if (reloadFlag) {
+ URL url = classLoader.getResource(resourceName);
+ if (url != null) {
+ URLConnection connection = url.openConnection();
+ if (connection != null) {
+ connection.setUseCaches(false);
+ is = connection.getInputStream();
+ }
+ }
+ }
+ else {
+ is = classLoader.getResourceAsStream(resourceName);
+ }
+ return is;
+ }
+ });
+ }
+ catch (PrivilegedActionException ex) {
+ throw (IOException) ex.getException();
+ }
+ if (stream != null) {
+ try {
+ return (defaultEncoding != null ?
+ new PropertyResourceBundle(new InputStreamReader(stream, defaultEncoding)) :
+ new PropertyResourceBundle(stream));
+ }
+ finally {
+ stream.close();
+ }
+ }
+ else {
+ return null;
+ }
+ }
+ else {
+ return super.newBundle(baseName, locale, format, loader, reload);
+ }
+ }
+
+ @Override
+ public Locale getFallbackLocale(String baseName, Locale locale) {
+ return (fallbackToSystemLocale ? super.getFallbackLocale(baseName, locale) : null);
+ }
+
+ @Override
+ public long getTimeToLive(String baseName, Locale locale) {
+ return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale));
+ }
+
+ @Override
+ public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {
+ if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) {
+ cachedBundleMessageFormats.remove(bundle);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ }
+
}
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 a2445ee981..8a462c0425 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * Copyright 2002-2012 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.
@@ -19,6 +19,7 @@ package org.springframework.context.support;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
+import java.util.ResourceBundle;
import junit.framework.TestCase;
@@ -46,6 +47,20 @@ public class ResourceBundleMessageSourceTests extends TestCase {
doTestMessageAccess(false, true, true, true, false);
}
+ public void testMessageAccessWithDefaultMessageSourceAndFallbackTurnedOff() {
+ if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {
+ return;
+ }
+ doTestMessageAccess(false, false, false, false, false);
+ }
+
+ public void testMessageAccessWithDefaultMessageSourceAndFallbackTurnedOffAndFallbackToGerman() {
+ if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {
+ return;
+ }
+ doTestMessageAccess(false, false, true, true, false);
+ }
+
public void testMessageAccessWithReloadableMessageSource() {
doTestMessageAccess(true, true, false, false, false);
}
@@ -82,13 +97,13 @@ public class ResourceBundleMessageSourceTests extends TestCase {
String[] basenames = null;
if (reloadable) {
basenames = new String[] {
- "classpath:" + basepath + "messages",
- "classpath:" + basepath + "more-messages"};
+ "classpath:" + basepath + "messages",
+ "classpath:" + basepath + "more-messages"};
}
else {
basenames = new String[] {
- basepath + "messages",
- basepath + "more-messages"};
+ basepath + "messages",
+ basepath + "more-messages"};
}
pvs.add("basenames", basenames);
if (!fallbackToSystemLocale) {
@@ -202,6 +217,31 @@ public class ResourceBundleMessageSourceTests extends TestCase {
assertEquals("nachricht2", ms.getMessage("code2", null, Locale.GERMAN));
}
+ public void testResourceBundleMessageSourceWithDefaultCharset() {
+ ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
+ ms.setBasename("org/springframework/context/support/messages");
+ ms.setDefaultEncoding("ISO-8859-1");
+ assertEquals("message1", ms.getMessage("code1", null, Locale.ENGLISH));
+ assertEquals("nachricht2", ms.getMessage("code2", null, Locale.GERMAN));
+ }
+
+ public void testResourceBundleMessageSourceWithInappropriateDefaultCharset() {
+ if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {
+ return;
+ }
+ ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
+ ms.setBasename("org/springframework/context/support/messages");
+ ms.setDefaultEncoding("argh");
+ ms.setFallbackToSystemLocale(false);
+ try {
+ ms.getMessage("code1", null, Locale.ENGLISH);
+ fail("Should have thrown NoSuchMessageException");
+ }
+ catch (NoSuchMessageException ex) {
+ // expected
+ }
+ }
+
public void testReloadableResourceBundleMessageSourceStandalone() {
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
ms.setBasename("org/springframework/context/support/messages");
@@ -300,4 +340,11 @@ public class ResourceBundleMessageSourceTests extends TestCase {
assertEquals(0, filenames.size());
}
+ @Override
+ protected void tearDown() throws Exception {
+ if (JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_16) {
+ ResourceBundle.clearCache();
+ }
+ }
+
}