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