1616
1717package 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 ;
1927import java .text .MessageFormat ;
2028import java .util .HashMap ;
2129import java .util .Locale ;
2230import java .util .Map ;
2331import java .util .MissingResourceException ;
32+ import java .util .PropertyResourceBundle ;
2433import java .util .ResourceBundle ;
2534
2635import org .springframework .beans .factory .BeanClassLoaderAware ;
36+ import org .springframework .core .JdkVersion ;
2737import org .springframework .util .Assert ;
2838import org .springframework .util .ClassUtils ;
2939import 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