001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.context.support; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.InputStreamReader; 022import java.io.Reader; 023import java.net.URL; 024import java.net.URLConnection; 025import java.security.AccessController; 026import java.security.PrivilegedActionException; 027import java.security.PrivilegedExceptionAction; 028import java.text.MessageFormat; 029import java.util.HashMap; 030import java.util.Locale; 031import java.util.Map; 032import java.util.MissingResourceException; 033import java.util.PropertyResourceBundle; 034import java.util.ResourceBundle; 035import java.util.Set; 036 037import org.springframework.beans.factory.BeanClassLoaderAware; 038import org.springframework.util.ClassUtils; 039 040/** 041 * {@link org.springframework.context.MessageSource} implementation that 042 * accesses resource bundles using specified basenames. This class relies 043 * on the underlying JDK's {@link java.util.ResourceBundle} implementation, 044 * in combination with the JDK's standard message parsing provided by 045 * {@link java.text.MessageFormat}. 046 * 047 * <p>This MessageSource caches both the accessed ResourceBundle instances and 048 * the generated MessageFormats for each message. It also implements rendering of 049 * no-arg messages without MessageFormat, as supported by the AbstractMessageSource 050 * base class. The caching provided by this MessageSource is significantly faster 051 * than the built-in caching of the {@code java.util.ResourceBundle} class. 052 * 053 * <p>The basenames follow {@link java.util.ResourceBundle} conventions: essentially, 054 * a fully-qualified classpath location. If it doesn't contain a package qualifier 055 * (such as {@code org.mypackage}), it will be resolved from the classpath root. 056 * Note that the JDK's standard ResourceBundle treats dots as package separators: 057 * This means that "test.theme" is effectively equivalent to "test/theme". 058 * 059 * @author Rod Johnson 060 * @author Juergen Hoeller 061 * @see #setBasenames 062 * @see ReloadableResourceBundleMessageSource 063 * @see java.util.ResourceBundle 064 * @see java.text.MessageFormat 065 */ 066public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware { 067 068 private ClassLoader bundleClassLoader; 069 070 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 071 072 /** 073 * Cache to hold loaded ResourceBundles. 074 * This Map is keyed with the bundle basename, which holds a Map that is 075 * keyed with the Locale and in turn holds the ResourceBundle instances. 076 * This allows for very efficient hash lookups, significantly faster 077 * than the ResourceBundle class's own cache. 078 */ 079 private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles = 080 new HashMap<String, Map<Locale, ResourceBundle>>(); 081 082 /** 083 * Cache to hold already generated MessageFormats. 084 * This Map is keyed with the ResourceBundle, which holds a Map that is 085 * keyed with the message code, which in turn holds a Map that is keyed 086 * with the Locale and holds the MessageFormat values. This allows for 087 * very efficient hash lookups without concatenated keys. 088 * @see #getMessageFormat 089 */ 090 private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats = 091 new HashMap<ResourceBundle, Map<String, Map<Locale, MessageFormat>>>(); 092 093 094 /** 095 * Set the ClassLoader to load resource bundles with. 096 * <p>Default is the containing BeanFactory's 097 * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader}, 098 * or the default ClassLoader determined by 099 * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()} 100 * if not running within a BeanFactory. 101 */ 102 public void setBundleClassLoader(ClassLoader classLoader) { 103 this.bundleClassLoader = classLoader; 104 } 105 106 /** 107 * Return the ClassLoader to load resource bundles with. 108 * <p>Default is the containing BeanFactory's bean ClassLoader. 109 * @see #setBundleClassLoader 110 */ 111 protected ClassLoader getBundleClassLoader() { 112 return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader); 113 } 114 115 @Override 116 public void setBeanClassLoader(ClassLoader classLoader) { 117 this.beanClassLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); 118 } 119 120 121 /** 122 * Resolves the given message code as key in the registered resource bundles, 123 * returning the value found in the bundle as-is (without MessageFormat parsing). 124 */ 125 @Override 126 protected String resolveCodeWithoutArguments(String code, Locale locale) { 127 Set<String> basenames = getBasenameSet(); 128 for (String basename : basenames) { 129 ResourceBundle bundle = getResourceBundle(basename, locale); 130 if (bundle != null) { 131 String result = getStringOrNull(bundle, code); 132 if (result != null) { 133 return result; 134 } 135 } 136 } 137 return null; 138 } 139 140 /** 141 * Resolves the given message code as key in the registered resource bundles, 142 * using a cached MessageFormat instance per message code. 143 */ 144 @Override 145 protected MessageFormat resolveCode(String code, Locale locale) { 146 Set<String> basenames = getBasenameSet(); 147 for (String basename : basenames) { 148 ResourceBundle bundle = getResourceBundle(basename, locale); 149 if (bundle != null) { 150 MessageFormat messageFormat = getMessageFormat(bundle, code, locale); 151 if (messageFormat != null) { 152 return messageFormat; 153 } 154 } 155 } 156 return null; 157 } 158 159 160 /** 161 * Return a ResourceBundle for the given basename and code, 162 * fetching already generated MessageFormats from the cache. 163 * @param basename the basename of the ResourceBundle 164 * @param locale the Locale to find the ResourceBundle for 165 * @return the resulting ResourceBundle, or {@code null} if none 166 * found for the given basename and Locale 167 */ 168 protected ResourceBundle getResourceBundle(String basename, Locale locale) { 169 if (getCacheMillis() >= 0) { 170 // Fresh ResourceBundle.getBundle call in order to let ResourceBundle 171 // do its native caching, at the expense of more extensive lookup steps. 172 return doGetBundle(basename, locale); 173 } 174 else { 175 // Cache forever: prefer locale cache over repeated getBundle calls. 176 synchronized (this.cachedResourceBundles) { 177 Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename); 178 if (localeMap != null) { 179 ResourceBundle bundle = localeMap.get(locale); 180 if (bundle != null) { 181 return bundle; 182 } 183 } 184 try { 185 ResourceBundle bundle = doGetBundle(basename, locale); 186 if (localeMap == null) { 187 localeMap = new HashMap<Locale, ResourceBundle>(); 188 this.cachedResourceBundles.put(basename, localeMap); 189 } 190 localeMap.put(locale, bundle); 191 return bundle; 192 } 193 catch (MissingResourceException ex) { 194 if (logger.isWarnEnabled()) { 195 logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); 196 } 197 // Assume bundle not found 198 // -> do NOT throw the exception to allow for checking parent message source. 199 return null; 200 } 201 } 202 } 203 } 204 205 /** 206 * Obtain the resource bundle for the given basename and Locale. 207 * @param basename the basename to look for 208 * @param locale the Locale to look for 209 * @return the corresponding ResourceBundle 210 * @throws MissingResourceException if no matching bundle could be found 211 * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader) 212 * @see #getBundleClassLoader() 213 */ 214 protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { 215 return ResourceBundle.getBundle(basename, locale, getBundleClassLoader(), new MessageSourceControl()); 216 } 217 218 /** 219 * Load a property-based resource bundle from the given reader. 220 * <p>The default implementation returns a {@link PropertyResourceBundle}. 221 * @param reader the reader for the target resource 222 * @return the fully loaded bundle 223 * @throws IOException in case of I/O failure 224 * @since 4.2 225 * @see PropertyResourceBundle#PropertyResourceBundle(Reader) 226 */ 227 protected ResourceBundle loadBundle(Reader reader) throws IOException { 228 return new PropertyResourceBundle(reader); 229 } 230 231 /** 232 * Return a MessageFormat for the given bundle and code, 233 * fetching already generated MessageFormats from the cache. 234 * @param bundle the ResourceBundle to work on 235 * @param code the message code to retrieve 236 * @param locale the Locale to use to build the MessageFormat 237 * @return the resulting MessageFormat, or {@code null} if no message 238 * defined for the given code 239 * @throws MissingResourceException if thrown by the ResourceBundle 240 */ 241 protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) 242 throws MissingResourceException { 243 244 synchronized (this.cachedBundleMessageFormats) { 245 Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle); 246 Map<Locale, MessageFormat> localeMap = null; 247 if (codeMap != null) { 248 localeMap = codeMap.get(code); 249 if (localeMap != null) { 250 MessageFormat result = localeMap.get(locale); 251 if (result != null) { 252 return result; 253 } 254 } 255 } 256 257 String msg = getStringOrNull(bundle, code); 258 if (msg != null) { 259 if (codeMap == null) { 260 codeMap = new HashMap<String, Map<Locale, MessageFormat>>(); 261 this.cachedBundleMessageFormats.put(bundle, codeMap); 262 } 263 if (localeMap == null) { 264 localeMap = new HashMap<Locale, MessageFormat>(); 265 codeMap.put(code, localeMap); 266 } 267 MessageFormat result = createMessageFormat(msg, locale); 268 localeMap.put(locale, result); 269 return result; 270 } 271 272 return null; 273 } 274 } 275 276 /** 277 * Efficiently retrieve the String value for the specified key, 278 * or return {@code null} if not found. 279 * <p>As of 4.2, the default implementation checks {@code containsKey} 280 * before it attempts to call {@code getString} (which would require 281 * catching {@code MissingResourceException} for key not found). 282 * <p>Can be overridden in subclasses. 283 * @param bundle the ResourceBundle to perform the lookup in 284 * @param key the key to look up 285 * @return the associated value, or {@code null} if none 286 * @since 4.2 287 * @see ResourceBundle#getString(String) 288 * @see ResourceBundle#containsKey(String) 289 */ 290 protected String getStringOrNull(ResourceBundle bundle, String key) { 291 if (bundle.containsKey(key)) { 292 try { 293 return bundle.getString(key); 294 } 295 catch (MissingResourceException ex) { 296 // Assume key not found for some other reason 297 // -> do NOT throw the exception to allow for checking parent message source. 298 } 299 } 300 return null; 301 } 302 303 /** 304 * Show the configuration of this MessageSource. 305 */ 306 @Override 307 public String toString() { 308 return getClass().getName() + ": basenames=" + getBasenameSet(); 309 } 310 311 312 /** 313 * Custom implementation of Java 6's {@code ResourceBundle.Control}, 314 * adding support for custom file encodings, deactivating the fallback to the 315 * system locale and activating ResourceBundle's native cache, if desired. 316 */ 317 private class MessageSourceControl extends ResourceBundle.Control { 318 319 @Override 320 public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) 321 throws IllegalAccessException, InstantiationException, IOException { 322 323 // Special handling of default encoding 324 if (format.equals("java.properties")) { 325 String bundleName = toBundleName(baseName, locale); 326 final String resourceName = toResourceName(bundleName, "properties"); 327 final ClassLoader classLoader = loader; 328 final boolean reloadFlag = reload; 329 InputStream stream; 330 try { 331 stream = AccessController.doPrivileged( 332 new PrivilegedExceptionAction<InputStream>() { 333 @Override 334 public InputStream run() throws IOException { 335 InputStream is = null; 336 if (reloadFlag) { 337 URL url = classLoader.getResource(resourceName); 338 if (url != null) { 339 URLConnection connection = url.openConnection(); 340 if (connection != null) { 341 connection.setUseCaches(false); 342 is = connection.getInputStream(); 343 } 344 } 345 } 346 else { 347 is = classLoader.getResourceAsStream(resourceName); 348 } 349 return is; 350 } 351 }); 352 } 353 catch (PrivilegedActionException ex) { 354 throw (IOException) ex.getException(); 355 } 356 if (stream != null) { 357 String encoding = getDefaultEncoding(); 358 if (encoding == null) { 359 encoding = "ISO-8859-1"; 360 } 361 try { 362 return loadBundle(new InputStreamReader(stream, encoding)); 363 } 364 finally { 365 stream.close(); 366 } 367 } 368 else { 369 return null; 370 } 371 } 372 else { 373 // Delegate handling of "java.class" format to standard Control 374 return super.newBundle(baseName, locale, format, loader, reload); 375 } 376 } 377 378 @Override 379 public Locale getFallbackLocale(String baseName, Locale locale) { 380 return (isFallbackToSystemLocale() ? super.getFallbackLocale(baseName, locale) : null); 381 } 382 383 @Override 384 public long getTimeToLive(String baseName, Locale locale) { 385 long cacheMillis = getCacheMillis(); 386 return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale)); 387 } 388 389 @Override 390 public boolean needsReload( 391 String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) { 392 393 if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) { 394 synchronized (cachedBundleMessageFormats) { 395 cachedBundleMessageFormats.remove(bundle); 396 } 397 return true; 398 } 399 else { 400 return false; 401 } 402 } 403 } 404 405}