ResourceBundleMessageSource supports "defaultEncoding", "fallbackToSystemLocale", "cacheSeconds"
These features require Java 6 or higher due to their dependency on the ResourceBundle.Control class. To some degree, ResourceBundleMessageSource catches up with ReloadableResourceBundleMessageSource now. However, as noted in the javadoc, there are still severe limitations in the standard ResourceBundle class that justify an ongoing investment in our own ReloadableResourceBundleMessageSource (based on the Spring resource abstraction, with manual parsing of properties files). Issue: SPR-7392
This commit is contained in:
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>Default is none, using the <code>java.util.Properties</code>
|
||||
* default encoding.
|
||||
* default encoding: ISO-8859-1.
|
||||
* <p>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").
|
||||
* <p>Falling back to the system Locale is the default behavior of
|
||||
* <code>java.util.ResourceBundle</code>. 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.
|
||||
* <code>java.util.ResourceBundle</code>. 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()) {
|
||||
|
||||
@@ -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.
|
||||
* <p>Default is none, using the <code>java.util.ResourceBundle</code>
|
||||
* default encoding: ISO-8859-1.
|
||||
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> 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").
|
||||
* <p>Falling back to the system Locale is the default behavior of
|
||||
* <code>java.util.ResourceBundle</code>. 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.
|
||||
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> 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.
|
||||
* <ul>
|
||||
* <li>Default is "-1", indicating to cache forever.
|
||||
* <li>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.
|
||||
* <li>A value of "0" will check the last-modified timestamp of the file on
|
||||
* every message access. <b>Do not use this in a production environment!</b>
|
||||
* <li><b>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.</b>
|
||||
* Consider {@link ReloadableResourceBundleMessageSource} in combination
|
||||
* with resource bundle files in a non-classpath location.
|
||||
* </ul>
|
||||
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> 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.
|
||||
* <p>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<Locale, ResourceBundle> 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<Locale, ResourceBundle> 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<Locale, ResourceBundle>();
|
||||
this.cachedResourceBundles.put(basename, localeMap);
|
||||
}
|
||||
localeMap.put(locale, bundle);
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
try {
|
||||
ResourceBundle bundle = doGetBundle(basename, locale);
|
||||
if (localeMap == null) {
|
||||
localeMap = new HashMap<Locale, ResourceBundle>();
|
||||
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 <code>ResourceBundle.Control</code>,
|
||||
* 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<InputStream>() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user