Skip to content

Commit c81543e

Browse files
committed
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
1 parent 437ce9b commit c81543e

File tree

3 files changed

+276
-40
lines changed

3 files changed

+276
-40
lines changed

org.springframework.context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,16 @@
3737
import org.springframework.util.StringUtils;
3838

3939
/**
40-
* {@link org.springframework.context.MessageSource} implementation that
41-
* accesses resource bundles using specified basenames. This class uses
42-
* {@link java.util.Properties} instances as its custom data structure for
43-
* messages, loading them via a {@link org.springframework.util.PropertiesPersister}
44-
* strategy: The default strategy is capable of loading properties files
45-
* with a specific character encoding, if desired.
40+
* Spring-specific {@link org.springframework.context.MessageSource} implementation
41+
* that accesses resource bundles using specified basenames, participating in the
42+
* Spring {@link org.springframework.context.ApplicationContext}'s resource loading.
43+
*
44+
* <p>In contrast to the JDK-based {@link ResourceBundleMessageSource}, this class uses
45+
* {@link java.util.Properties} instances as its custom data structure for messages,
46+
* loading them via a {@link org.springframework.util.PropertiesPersister} strategy
47+
* from Spring {@link Resource} handles. This strategy is not only capable of
48+
* reloading files based on timestamp changes, but also of loading properties files
49+
* with a specific character encoding. It will detect XML property files as well.
4650
*
4751
* <p>In contrast to {@link ResourceBundleMessageSource}, this class supports
4852
* reloading of properties files through the {@link #setCacheSeconds "cacheSeconds"}
@@ -171,7 +175,7 @@ public void setBasenames(String... basenames) {
171175
* Set the default charset to use for parsing properties files.
172176
* Used if no file-specific charset is specified for a file.
173177
* <p>Default is none, using the <code>java.util.Properties</code>
174-
* default encoding.
178+
* default encoding: ISO-8859-1.
175179
* <p>Only applies to classic properties files, not to XML files.
176180
* @param defaultEncoding the default charset
177181
* @see #setFileEncodings
@@ -201,10 +205,9 @@ public void setFileEncodings(Properties fileEncodings) {
201205
* fallback will be the default file (e.g. "messages.properties" for
202206
* basename "messages").
203207
* <p>Falling back to the system Locale is the default behavior of
204-
* <code>java.util.ResourceBundle</code>. However, this is often not
205-
* desirable in an application server environment, where the system Locale
206-
* is not relevant to the application at all: Set this flag to "false"
207-
* in such a scenario.
208+
* <code>java.util.ResourceBundle</code>. However, this is often not desirable
209+
* in an application server environment, where the system Locale is not relevant
210+
* to the application at all: Set this flag to "false" in such a scenario.
208211
*/
209212
public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
210213
this.fallbackToSystemLocale = fallbackToSystemLocale;
@@ -448,7 +451,7 @@ protected PropertiesHolder getProperties(String filename) {
448451
* @param propHolder the current PropertiesHolder for the bundle
449452
*/
450453
protected PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
451-
long refreshTimestamp = (this.cacheMillis < 0) ? -1 : System.currentTimeMillis();
454+
long refreshTimestamp = (this.cacheMillis < 0 ? -1 : System.currentTimeMillis());
452455

453456
Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
454457
if (!resource.exists()) {

org.springframework.context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java

Lines changed: 209 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@
1616

1717
package org.springframework.context.support;
1818

19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.InputStreamReader;
22+
import java.net.URL;
23+
import java.net.URLConnection;
24+
import java.security.AccessController;
25+
import java.security.PrivilegedActionException;
26+
import java.security.PrivilegedExceptionAction;
1927
import java.text.MessageFormat;
2028
import java.util.HashMap;
2129
import java.util.Locale;
2230
import java.util.Map;
2331
import java.util.MissingResourceException;
32+
import java.util.PropertyResourceBundle;
2433
import java.util.ResourceBundle;
2534

2635
import org.springframework.beans.factory.BeanClassLoaderAware;
36+
import org.springframework.core.JdkVersion;
2737
import org.springframework.util.Assert;
2838
import org.springframework.util.ClassUtils;
2939
import org.springframework.util.StringUtils;
@@ -58,6 +68,12 @@ public class ResourceBundleMessageSource extends AbstractMessageSource implement
5868

5969
private String[] basenames = new String[0];
6070

71+
private String defaultEncoding;
72+
73+
private boolean fallbackToSystemLocale = true;
74+
75+
private long cacheMillis = -1;
76+
6177
private ClassLoader bundleClassLoader;
6278

6379
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
@@ -133,6 +149,59 @@ public void setBasenames(String... basenames) {
133149
}
134150
}
135151

152+
/**
153+
* Set the default charset to use for parsing resource bundle files.
154+
* <p>Default is none, using the <code>java.util.ResourceBundle</code>
155+
* default encoding: ISO-8859-1.
156+
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> Consider using
157+
* {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support
158+
* and more flexibility in setting of an encoding per file.
159+
*/
160+
public void setDefaultEncoding(String defaultEncoding) {
161+
this.defaultEncoding = defaultEncoding;
162+
}
163+
164+
/**
165+
* Set whether to fall back to the system Locale if no files for a specific
166+
* Locale have been found. Default is "true"; if this is turned off, the only
167+
* fallback will be the default file (e.g. "messages.properties" for
168+
* basename "messages").
169+
* <p>Falling back to the system Locale is the default behavior of
170+
* <code>java.util.ResourceBundle</code>. However, this is often not desirable
171+
* in an application server environment, where the system Locale is not relevant
172+
* to the application at all: Set this flag to "false" in such a scenario.
173+
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> Consider using
174+
* {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support.
175+
*/
176+
public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
177+
this.fallbackToSystemLocale = fallbackToSystemLocale;
178+
}
179+
180+
/**
181+
* Set the number of seconds to cache loaded resource bundle files.
182+
* <ul>
183+
* <li>Default is "-1", indicating to cache forever.
184+
* <li>A positive number will expire resource bundles after the given
185+
* number of seconds. This is essentially the interval between refresh checks.
186+
* Note that a refresh attempt will first check the last-modified timestamp
187+
* of the file before actually reloading it; so if files don't change, this
188+
* interval can be set rather low, as refresh attempts will not actually reload.
189+
* <li>A value of "0" will check the last-modified timestamp of the file on
190+
* every message access. <b>Do not use this in a production environment!</b>
191+
* <li><b>Note that depending on your ClassLoader, expiration might not work reliably
192+
* since the ClassLoader may hold on to a cached version of the bundle file.</b>
193+
* Consider {@link ReloadableResourceBundleMessageSource} in combination
194+
* with resource bundle files in a non-classpath location.
195+
* </ul>
196+
* <p><b>NOTE: Only works on JDK 1.6 and higher.</b> Consider using
197+
* {@link ReloadableResourceBundleMessageSource} for JDK 1.5 support
198+
* and more flexibility in terms of the kinds of resources to load from
199+
* (in particular from outside of the classpath where expiration works reliably).
200+
*/
201+
public void setCacheSeconds(int cacheSeconds) {
202+
this.cacheMillis = (cacheSeconds * 1000);
203+
}
204+
136205
/**
137206
* Set the ClassLoader to load resource bundles with.
138207
* <p>Default is the containing BeanFactory's
@@ -201,30 +270,38 @@ protected MessageFormat resolveCode(String code, Locale locale) {
201270
* found for the given basename and Locale
202271
*/
203272
protected ResourceBundle getResourceBundle(String basename, Locale locale) {
204-
synchronized (this.cachedResourceBundles) {
205-
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
206-
if (localeMap != null) {
207-
ResourceBundle bundle = localeMap.get(locale);
208-
if (bundle != null) {
209-
return bundle;
273+
if (this.cacheMillis >= 0) {
274+
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
275+
// do its native caching, at the expense of more extensive lookup steps.
276+
return doGetBundle(basename, locale);
277+
}
278+
else {
279+
// Cache forever: prefer locale cache over repeated getBundle calls.
280+
synchronized (this.cachedResourceBundles) {
281+
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
282+
if (localeMap != null) {
283+
ResourceBundle bundle = localeMap.get(locale);
284+
if (bundle != null) {
285+
return bundle;
286+
}
210287
}
211-
}
212-
try {
213-
ResourceBundle bundle = doGetBundle(basename, locale);
214-
if (localeMap == null) {
215-
localeMap = new HashMap<Locale, ResourceBundle>();
216-
this.cachedResourceBundles.put(basename, localeMap);
288+
try {
289+
ResourceBundle bundle = doGetBundle(basename, locale);
290+
if (localeMap == null) {
291+
localeMap = new HashMap<Locale, ResourceBundle>();
292+
this.cachedResourceBundles.put(basename, localeMap);
293+
}
294+
localeMap.put(locale, bundle);
295+
return bundle;
217296
}
218-
localeMap.put(locale, bundle);
219-
return bundle;
220-
}
221-
catch (MissingResourceException ex) {
222-
if (logger.isWarnEnabled()) {
223-
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
297+
catch (MissingResourceException ex) {
298+
if (logger.isWarnEnabled()) {
299+
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
300+
}
301+
// Assume bundle not found
302+
// -> do NOT throw the exception to allow for checking parent message source.
303+
return null;
224304
}
225-
// Assume bundle not found
226-
// -> do NOT throw the exception to allow for checking parent message source.
227-
return null;
228305
}
229306
}
230307
}
@@ -239,7 +316,20 @@ protected ResourceBundle getResourceBundle(String basename, Locale locale) {
239316
* @see #getBundleClassLoader()
240317
*/
241318
protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException {
242-
return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
319+
if ((this.defaultEncoding != null && !"ISO-8859-1".equals(this.defaultEncoding)) ||
320+
!this.fallbackToSystemLocale || this.cacheMillis >= 0) {
321+
// Custom Control required...
322+
if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_16) {
323+
throw new IllegalStateException("Cannot use 'defaultEncoding', 'fallbackToSystemLocale' and " +
324+
"'cacheSeconds' on the standard ResourceBundleMessageSource when running on Java 5. " +
325+
"Consider using ReloadableResourceBundleMessageSource instead.");
326+
}
327+
return new ControlBasedResourceBundleFactory().getBundle(basename, locale);
328+
}
329+
else {
330+
// Good old standard call...
331+
return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
332+
}
243333
}
244334

245335
/**
@@ -298,7 +388,6 @@ private String getStringOrNull(ResourceBundle bundle, String key) {
298388
}
299389
}
300390

301-
302391
/**
303392
* Show the configuration of this MessageSource.
304393
*/
@@ -308,4 +397,101 @@ public String toString() {
308397
StringUtils.arrayToCommaDelimitedString(this.basenames) + "]";
309398
}
310399

400+
401+
/**
402+
* Factory indirection for runtime isolation of the optional dependencv on
403+
* Java 6's Control class.
404+
* @see ResourceBundle#getBundle(String, java.util.Locale, ClassLoader, java.util.ResourceBundle.Control)
405+
* @see MessageSourceControl
406+
*/
407+
private class ControlBasedResourceBundleFactory {
408+
409+
public ResourceBundle getBundle(String basename, Locale locale) {
410+
return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), new MessageSourceControl());
411+
}
412+
}
413+
414+
415+
/**
416+
* Custom implementation of Java 6's <code>ResourceBundle.Control</code>,
417+
* adding support for custom file encodings, deactivating the fallback to the
418+
* system locale and activating ResourceBundle's native cache, if desired.
419+
*/
420+
private class MessageSourceControl extends ResourceBundle.Control {
421+
422+
@Override
423+
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
424+
throws IllegalAccessException, InstantiationException, IOException {
425+
if (format.equals("java.properties")) {
426+
String bundleName = toBundleName(baseName, locale);
427+
final String resourceName = toResourceName(bundleName, "properties");
428+
final ClassLoader classLoader = loader;
429+
final boolean reloadFlag = reload;
430+
InputStream stream;
431+
try {
432+
stream = AccessController.doPrivileged(
433+
new PrivilegedExceptionAction<InputStream>() {
434+
public InputStream run() throws IOException {
435+
InputStream is = null;
436+
if (reloadFlag) {
437+
URL url = classLoader.getResource(resourceName);
438+
if (url != null) {
439+
URLConnection connection = url.openConnection();
440+
if (connection != null) {
441+
connection.setUseCaches(false);
442+
is = connection.getInputStream();
443+
}
444+
}
445+
}
446+
else {
447+
is = classLoader.getResourceAsStream(resourceName);
448+
}
449+
return is;
450+
}
451+
});
452+
}
453+
catch (PrivilegedActionException ex) {
454+
throw (IOException) ex.getException();
455+
}
456+
if (stream != null) {
457+
try {
458+
return (defaultEncoding != null ?
459+
new PropertyResourceBundle(new InputStreamReader(stream, defaultEncoding)) :
460+
new PropertyResourceBundle(stream));
461+
}
462+
finally {
463+
stream.close();
464+
}
465+
}
466+
else {
467+
return null;
468+
}
469+
}
470+
else {
471+
return super.newBundle(baseName, locale, format, loader, reload);
472+
}
473+
}
474+
475+
@Override
476+
public Locale getFallbackLocale(String baseName, Locale locale) {
477+
return (fallbackToSystemLocale ? super.getFallbackLocale(baseName, locale) : null);
478+
}
479+
480+
@Override
481+
public long getTimeToLive(String baseName, Locale locale) {
482+
return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale));
483+
}
484+
485+
@Override
486+
public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) {
487+
if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) {
488+
cachedBundleMessageFormats.remove(bundle);
489+
return true;
490+
}
491+
else {
492+
return false;
493+
}
494+
}
495+
}
496+
311497
}

0 commit comments

Comments
 (0)