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. + *

+ *

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(); + } + } + }