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.Locale; 030import java.util.Map; 031import java.util.MissingResourceException; 032import java.util.PropertyResourceBundle; 033import java.util.ResourceBundle; 034import java.util.Set; 035import java.util.concurrent.ConcurrentHashMap; 036 037import org.springframework.beans.factory.BeanClassLoaderAware; 038import org.springframework.lang.Nullable; 039import org.springframework.util.Assert; 040import org.springframework.util.ClassUtils; 041 042/** 043 * {@link org.springframework.context.MessageSource} implementation that 044 * accesses resource bundles using specified basenames. This class relies 045 * on the underlying JDK's {@link java.util.ResourceBundle} implementation, 046 * in combination with the JDK's standard message parsing provided by 047 * {@link java.text.MessageFormat}. 048 * 049 * <p>This MessageSource caches both the accessed ResourceBundle instances and 050 * the generated MessageFormats for each message. It also implements rendering of 051 * no-arg messages without MessageFormat, as supported by the AbstractMessageSource 052 * base class. The caching provided by this MessageSource is significantly faster 053 * than the built-in caching of the {@code java.util.ResourceBundle} class. 054 * 055 * <p>The basenames follow {@link java.util.ResourceBundle} conventions: essentially, 056 * a fully-qualified classpath location. If it doesn't contain a package qualifier 057 * (such as {@code org.mypackage}), it will be resolved from the classpath root. 058 * Note that the JDK's standard ResourceBundle treats dots as package separators: 059 * This means that "test.theme" is effectively equivalent to "test/theme". 060 * 061 * <p>On the classpath, bundle resources will be read with the locally configured 062 * {@link #setDefaultEncoding encoding}: by default, ISO-8859-1; consider switching 063 * this to UTF-8, or to {@code null} for the platform default encoding. On the JDK 9+ 064 * module path where locally provided {@code ResourceBundle.Control} handles are not 065 * supported, this MessageSource always falls back to {@link ResourceBundle#getBundle} 066 * retrieval with the platform default encoding: UTF-8 with a ISO-8859-1 fallback on 067 * JDK 9+ (configurable through the "java.util.PropertyResourceBundle.encoding" system 068 * property). Note that {@link #loadBundle(Reader)}/{@link #loadBundle(InputStream)} 069 * won't be called in this case either, effectively ignoring overrides in subclasses. 070 * Consider implementing a JDK 9 {@code java.util.spi.ResourceBundleProvider} instead. 071 * 072 * @author Rod Johnson 073 * @author Juergen Hoeller 074 * @see #setBasenames 075 * @see ReloadableResourceBundleMessageSource 076 * @see java.util.ResourceBundle 077 * @see java.text.MessageFormat 078 */ 079public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware { 080 081 @Nullable 082 private ClassLoader bundleClassLoader; 083 084 @Nullable 085 private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); 086 087 /** 088 * Cache to hold loaded ResourceBundles. 089 * This Map is keyed with the bundle basename, which holds a Map that is 090 * keyed with the Locale and in turn holds the ResourceBundle instances. 091 * This allows for very efficient hash lookups, significantly faster 092 * than the ResourceBundle class's own cache. 093 */ 094 private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles = 095 new ConcurrentHashMap<>(); 096 097 /** 098 * Cache to hold already generated MessageFormats. 099 * This Map is keyed with the ResourceBundle, which holds a Map that is 100 * keyed with the message code, which in turn holds a Map that is keyed 101 * with the Locale and holds the MessageFormat values. This allows for 102 * very efficient hash lookups without concatenated keys. 103 * @see #getMessageFormat 104 */ 105 private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats = 106 new ConcurrentHashMap<>(); 107 108 @Nullable 109 private volatile MessageSourceControl control = new MessageSourceControl(); 110 111 112 public ResourceBundleMessageSource() { 113 setDefaultEncoding("ISO-8859-1"); 114 } 115 116 117 /** 118 * Set the ClassLoader to load resource bundles with. 119 * <p>Default is the containing BeanFactory's 120 * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader}, 121 * or the default ClassLoader determined by 122 * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()} 123 * if not running within a BeanFactory. 124 */ 125 public void setBundleClassLoader(ClassLoader classLoader) { 126 this.bundleClassLoader = classLoader; 127 } 128 129 /** 130 * Return the ClassLoader to load resource bundles with. 131 * <p>Default is the containing BeanFactory's bean ClassLoader. 132 * @see #setBundleClassLoader 133 */ 134 @Nullable 135 protected ClassLoader getBundleClassLoader() { 136 return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader); 137 } 138 139 @Override 140 public void setBeanClassLoader(ClassLoader classLoader) { 141 this.beanClassLoader = classLoader; 142 } 143 144 145 /** 146 * Resolves the given message code as key in the registered resource bundles, 147 * returning the value found in the bundle as-is (without MessageFormat parsing). 148 */ 149 @Override 150 protected String resolveCodeWithoutArguments(String code, Locale locale) { 151 Set<String> basenames = getBasenameSet(); 152 for (String basename : basenames) { 153 ResourceBundle bundle = getResourceBundle(basename, locale); 154 if (bundle != null) { 155 String result = getStringOrNull(bundle, code); 156 if (result != null) { 157 return result; 158 } 159 } 160 } 161 return null; 162 } 163 164 /** 165 * Resolves the given message code as key in the registered resource bundles, 166 * using a cached MessageFormat instance per message code. 167 */ 168 @Override 169 @Nullable 170 protected MessageFormat resolveCode(String code, Locale locale) { 171 Set<String> basenames = getBasenameSet(); 172 for (String basename : basenames) { 173 ResourceBundle bundle = getResourceBundle(basename, locale); 174 if (bundle != null) { 175 MessageFormat messageFormat = getMessageFormat(bundle, code, locale); 176 if (messageFormat != null) { 177 return messageFormat; 178 } 179 } 180 } 181 return null; 182 } 183 184 185 /** 186 * Return a ResourceBundle for the given basename and code, 187 * fetching already generated MessageFormats from the cache. 188 * @param basename the basename of the ResourceBundle 189 * @param locale the Locale to find the ResourceBundle for 190 * @return the resulting ResourceBundle, or {@code null} if none 191 * found for the given basename and Locale 192 */ 193 @Nullable 194 protected ResourceBundle getResourceBundle(String basename, Locale locale) { 195 if (getCacheMillis() >= 0) { 196 // Fresh ResourceBundle.getBundle call in order to let ResourceBundle 197 // do its native caching, at the expense of more extensive lookup steps. 198 return doGetBundle(basename, locale); 199 } 200 else { 201 // Cache forever: prefer locale cache over repeated getBundle calls. 202 Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename); 203 if (localeMap != null) { 204 ResourceBundle bundle = localeMap.get(locale); 205 if (bundle != null) { 206 return bundle; 207 } 208 } 209 try { 210 ResourceBundle bundle = doGetBundle(basename, locale); 211 if (localeMap == null) { 212 localeMap = new ConcurrentHashMap<>(); 213 Map<Locale, ResourceBundle> existing = this.cachedResourceBundles.putIfAbsent(basename, localeMap); 214 if (existing != null) { 215 localeMap = existing; 216 } 217 } 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()); 224 } 225 // Assume bundle not found 226 // -> do NOT throw the exception to allow for checking parent message source. 227 return null; 228 } 229 } 230 } 231 232 /** 233 * Obtain the resource bundle for the given basename and Locale. 234 * @param basename the basename to look for 235 * @param locale the Locale to look for 236 * @return the corresponding ResourceBundle 237 * @throws MissingResourceException if no matching bundle could be found 238 * @see java.util.ResourceBundle#getBundle(String, Locale, ClassLoader) 239 * @see #getBundleClassLoader() 240 */ 241 protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { 242 ClassLoader classLoader = getBundleClassLoader(); 243 Assert.state(classLoader != null, "No bundle ClassLoader set"); 244 245 MessageSourceControl control = this.control; 246 if (control != null) { 247 try { 248 return ResourceBundle.getBundle(basename, locale, classLoader, control); 249 } 250 catch (UnsupportedOperationException ex) { 251 // Probably in a Jigsaw environment on JDK 9+ 252 this.control = null; 253 String encoding = getDefaultEncoding(); 254 if (encoding != null && logger.isInfoEnabled()) { 255 logger.info("ResourceBundleMessageSource is configured to read resources with encoding '" + 256 encoding + "' but ResourceBundle.Control not supported in current system environment: " + 257 ex.getMessage() + " - falling back to plain ResourceBundle.getBundle retrieval with the " + 258 "platform default encoding. Consider setting the 'defaultEncoding' property to 'null' " + 259 "for participating in the platform default and therefore avoiding this log message."); 260 } 261 } 262 } 263 264 // Fallback: plain getBundle lookup without Control handle 265 return ResourceBundle.getBundle(basename, locale, classLoader); 266 } 267 268 /** 269 * Load a property-based resource bundle from the given reader. 270 * <p>This will be called in case of a {@link #setDefaultEncoding "defaultEncoding"}, 271 * including {@link ResourceBundleMessageSource}'s default ISO-8859-1 encoding. 272 * Note that this method can only be called with a {@code ResourceBundle.Control}: 273 * When running on the JDK 9+ module path where such control handles are not 274 * supported, any overrides in custom subclasses will effectively get ignored. 275 * <p>The default implementation returns a {@link PropertyResourceBundle}. 276 * @param reader the reader for the target resource 277 * @return the fully loaded bundle 278 * @throws IOException in case of I/O failure 279 * @since 4.2 280 * @see #loadBundle(InputStream) 281 * @see PropertyResourceBundle#PropertyResourceBundle(Reader) 282 */ 283 protected ResourceBundle loadBundle(Reader reader) throws IOException { 284 return new PropertyResourceBundle(reader); 285 } 286 287 /** 288 * Load a property-based resource bundle from the given input stream, 289 * picking up the default properties encoding on JDK 9+. 290 * <p>This will only be called with {@link #setDefaultEncoding "defaultEncoding"} 291 * set to {@code null}, explicitly enforcing the platform default encoding 292 * (which is UTF-8 with a ISO-8859-1 fallback on JDK 9+ but configurable 293 * through the "java.util.PropertyResourceBundle.encoding" system property). 294 * Note that this method can only be called with a {@code ResourceBundle.Control}: 295 * When running on the JDK 9+ module path where such control handles are not 296 * supported, any overrides in custom subclasses will effectively get ignored. 297 * <p>The default implementation returns a {@link PropertyResourceBundle}. 298 * @param inputStream the input stream for the target resource 299 * @return the fully loaded bundle 300 * @throws IOException in case of I/O failure 301 * @since 5.1 302 * @see #loadBundle(Reader) 303 * @see PropertyResourceBundle#PropertyResourceBundle(InputStream) 304 */ 305 protected ResourceBundle loadBundle(InputStream inputStream) throws IOException { 306 return new PropertyResourceBundle(inputStream); 307 } 308 309 /** 310 * Return a MessageFormat for the given bundle and code, 311 * fetching already generated MessageFormats from the cache. 312 * @param bundle the ResourceBundle to work on 313 * @param code the message code to retrieve 314 * @param locale the Locale to use to build the MessageFormat 315 * @return the resulting MessageFormat, or {@code null} if no message 316 * defined for the given code 317 * @throws MissingResourceException if thrown by the ResourceBundle 318 */ 319 @Nullable 320 protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) 321 throws MissingResourceException { 322 323 Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle); 324 Map<Locale, MessageFormat> localeMap = null; 325 if (codeMap != null) { 326 localeMap = codeMap.get(code); 327 if (localeMap != null) { 328 MessageFormat result = localeMap.get(locale); 329 if (result != null) { 330 return result; 331 } 332 } 333 } 334 335 String msg = getStringOrNull(bundle, code); 336 if (msg != null) { 337 if (codeMap == null) { 338 codeMap = new ConcurrentHashMap<>(); 339 Map<String, Map<Locale, MessageFormat>> existing = 340 this.cachedBundleMessageFormats.putIfAbsent(bundle, codeMap); 341 if (existing != null) { 342 codeMap = existing; 343 } 344 } 345 if (localeMap == null) { 346 localeMap = new ConcurrentHashMap<>(); 347 Map<Locale, MessageFormat> existing = codeMap.putIfAbsent(code, localeMap); 348 if (existing != null) { 349 localeMap = existing; 350 } 351 } 352 MessageFormat result = createMessageFormat(msg, locale); 353 localeMap.put(locale, result); 354 return result; 355 } 356 357 return null; 358 } 359 360 /** 361 * Efficiently retrieve the String value for the specified key, 362 * or return {@code null} if not found. 363 * <p>As of 4.2, the default implementation checks {@code containsKey} 364 * before it attempts to call {@code getString} (which would require 365 * catching {@code MissingResourceException} for key not found). 366 * <p>Can be overridden in subclasses. 367 * @param bundle the ResourceBundle to perform the lookup in 368 * @param key the key to look up 369 * @return the associated value, or {@code null} if none 370 * @since 4.2 371 * @see ResourceBundle#getString(String) 372 * @see ResourceBundle#containsKey(String) 373 */ 374 @Nullable 375 protected String getStringOrNull(ResourceBundle bundle, String key) { 376 if (bundle.containsKey(key)) { 377 try { 378 return bundle.getString(key); 379 } 380 catch (MissingResourceException ex) { 381 // Assume key not found for some other reason 382 // -> do NOT throw the exception to allow for checking parent message source. 383 } 384 } 385 return null; 386 } 387 388 /** 389 * Show the configuration of this MessageSource. 390 */ 391 @Override 392 public String toString() { 393 return getClass().getName() + ": basenames=" + getBasenameSet(); 394 } 395 396 397 /** 398 * Custom implementation of {@code ResourceBundle.Control}, adding support 399 * for custom file encodings, deactivating the fallback to the system locale 400 * and activating ResourceBundle's native cache, if desired. 401 */ 402 private class MessageSourceControl extends ResourceBundle.Control { 403 404 @Override 405 @Nullable 406 public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) 407 throws IllegalAccessException, InstantiationException, IOException { 408 409 // Special handling of default encoding 410 if (format.equals("java.properties")) { 411 String bundleName = toBundleName(baseName, locale); 412 final String resourceName = toResourceName(bundleName, "properties"); 413 final ClassLoader classLoader = loader; 414 final boolean reloadFlag = reload; 415 InputStream inputStream; 416 try { 417 inputStream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> { 418 InputStream is = null; 419 if (reloadFlag) { 420 URL url = classLoader.getResource(resourceName); 421 if (url != null) { 422 URLConnection connection = url.openConnection(); 423 if (connection != null) { 424 connection.setUseCaches(false); 425 is = connection.getInputStream(); 426 } 427 } 428 } 429 else { 430 is = classLoader.getResourceAsStream(resourceName); 431 } 432 return is; 433 }); 434 } 435 catch (PrivilegedActionException ex) { 436 throw (IOException) ex.getException(); 437 } 438 if (inputStream != null) { 439 String encoding = getDefaultEncoding(); 440 if (encoding != null) { 441 try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) { 442 return loadBundle(bundleReader); 443 } 444 } 445 else { 446 try (InputStream bundleStream = inputStream) { 447 return loadBundle(bundleStream); 448 } 449 } 450 } 451 else { 452 return null; 453 } 454 } 455 else { 456 // Delegate handling of "java.class" format to standard Control 457 return super.newBundle(baseName, locale, format, loader, reload); 458 } 459 } 460 461 @Override 462 @Nullable 463 public Locale getFallbackLocale(String baseName, Locale locale) { 464 Locale defaultLocale = getDefaultLocale(); 465 return (defaultLocale != null && !defaultLocale.equals(locale) ? defaultLocale : null); 466 } 467 468 @Override 469 public long getTimeToLive(String baseName, Locale locale) { 470 long cacheMillis = getCacheMillis(); 471 return (cacheMillis >= 0 ? cacheMillis : super.getTimeToLive(baseName, locale)); 472 } 473 474 @Override 475 public boolean needsReload( 476 String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime) { 477 478 if (super.needsReload(baseName, locale, format, loader, bundle, loadTime)) { 479 cachedBundleMessageFormats.remove(bundle); 480 return true; 481 } 482 else { 483 return false; 484 } 485 } 486 } 487 488}