package org.dayatang.i18n.impl; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import java.text.MessageFormat; import java.util.*; public class ResourceBundleI18nService extends AbstractI18nService { private String[] basenames = new String[0]; private ClassLoader bundleClassLoader; private ClassLoader beanTypeLoader = getDefaultClassLoader(); /** * Cache to hold loaded ResourceBundles. This Map is keyed with the bundle * basename, which holds a Map that is keyed with the Locale and in turn * holds the ResourceBundle instances. This allows for very efficient hash * lookups, significantly faster than the ResourceBundle class's own cache. */ private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles = new HashMap<String, Map<Locale, ResourceBundle>>(); /** * Cache to hold already generated MessageFormats. This Map is keyed with * the ResourceBundle, which holds a Map that is keyed with the message * code, which in turn holds a Map that is keyed with the Locale and holds * the MessageFormat values. This allows for very efficient hash lookups * without concatenated keys. * * @see #getMessageFormat */ private final Map<ResourceBundle, Map<String, Map<Locale, MessageFormat>>> cachedBundleMessageFormats = new HashMap<ResourceBundle, Map<String, Map<Locale, MessageFormat>>>(); /** * Set a single basename, following {@link java.util.ResourceBundle} * conventions: essentially, a fully-qualified classpath location. If it * doesn't contain a package qualifier (such as <code>org.mypackage</code>), * it will be resolved from the classpath root. * <p> * Messages will normally be held in the "/lib" or "/classes" directory of a * web shiro's WAR structure. They can also be held in jar files on * the class path. * <p> * Note that ResourceBundle names are effectively classpath locations: As a * consequence, the JDK's standard ResourceBundle treats dots as package * separators. This means that "test.theme" is effectively equivalent to * "test/theme", just like it is for programmatic * <code>java.util.ResourceBundle</code> usage. * * @see #setBasenames * @see java.util.ResourceBundle#getBundle(String) */ public void setBasename(String basename) { setBasenames(new String[] { basename }); } /** * Set an array of basenames, each following * {@link java.util.ResourceBundle} conventions: essentially, a * fully-qualified classpath location. If it doesn't contain a package * qualifier (such as <code>org.mypackage</code>), it will be resolved from * the classpath root. * <p> * The associated resource bundles will be checked sequentially when * resolving a message code. Note that message definitions in a * <i>previous</i> resource bundle will override ones in a later bundle, due * to the sequential lookup. * <p> * Note that ResourceBundle names are effectively classpath locations: As a * consequence, the JDK's standard ResourceBundle treats dots as package * separators. This means that "test.theme" is effectively equivalent to * "test/theme", just like it is for programmatic * <code>java.util.ResourceBundle</code> usage. * * @see #setBasename * @see java.util.ResourceBundle#getBundle(String) */ public void setBasenames(String[] names) { if (names == null) { this.basenames = new String[0]; } else { this.basenames = Arrays.copyOf(names, names.length); } for (int i = 0; i < basenames.length; i++) { String basename = basenames[i]; if (StringUtils.isEmpty(basename) || StringUtils.isBlank(basename)) { throw new IllegalArgumentException( "Basename must not be null or empty"); } this.basenames[i] = basename.trim(); } } /** * Set the ClassLoader to load resource bundles with. * <p> * Default is the containing BeanFactory's * {@link org.springframework.beans.factory.BeanClassLoaderAware bean * ClassLoader}, or the default ClassLoader determined by * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()} if * not running within a BeanFactory. */ public void setBundleClassLoader(ClassLoader classLoader) { this.bundleClassLoader = classLoader; } /** * Return the ClassLoader to load resource bundles with. * <p> * Default is the containing BeanFactory's bean ClassLoader. * * @see #setBundleClassLoader */ protected ClassLoader getBundleClassLoader() { return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanTypeLoader); } public void setBeanClassLoader(ClassLoader classLoader) { this.beanTypeLoader = (classLoader != null ? classLoader : getDefaultClassLoader()); } /** * Resolves the given message code as key in the registered resource * bundles, returning the value found in the bundle as-is (without * MessageFormat parsing). */ @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { String result = null; for (int i = 0; result == null && i < this.basenames.length; i++) { ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); if (bundle != null) { result = getStringOrNull(bundle, code); } } return result; } /** * Resolves the given message code as key in the registered resource * bundles, using a cached MessageFormat instance per message code. */ @Override protected MessageFormat resolveCode(String code, Locale locale) { MessageFormat messageFormat = null; for (int i = 0; messageFormat == null && i < this.basenames.length; i++) { ResourceBundle bundle = getResourceBundle(this.basenames[i], locale); if (bundle != null) { messageFormat = getMessageFormat(bundle, code, locale); } } return messageFormat; } /** * Return a ResourceBundle for the given basename and code, fetching already * generated MessageFormats from the cache. * * @param basename * the basename of the ResourceBundle * @param locale * the Locale to find the ResourceBundle for * @return the resulting ResourceBundle, or <code>null</code> if none found * for the given basename and Locale */ protected ResourceBundle getResourceBundle(String basename, Locale locale) { synchronized (this.cachedResourceBundles) { Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles .get(basename); if (localeMap != null) { ResourceBundle bundle = localeMap.get(locale); if (bundle != null) { return bundle; } } try { ResourceBundle bundle = doGetBundle(basename, locale); if (localeMap == null) { localeMap = new HashMap<Locale, ResourceBundle>(); this.cachedResourceBundles.put(basename, localeMap); } localeMap.put(locale, bundle); return bundle; } catch (MissingResourceException ex) { if (logger.isWarnEnabled()) { logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage()); } // Assume bundle not found // -> do NOT throw the exception to allow for checking parent // message source. return null; } } } /** * Obtain the resource bundle for the given basename and Locale. * * @param basename * the basename to look for * @param locale * the Locale to look for * @return the corresponding ResourceBundle * @throws MissingResourceException * if no matching bundle could be found * @see java.util.ResourceBundle#getBundle(String, java.util.Locale, * ClassLoader) * @see #getBundleClassLoader() */ protected ResourceBundle doGetBundle(String basename, Locale locale) throws MissingResourceException { return ResourceBundle.getBundle(basename, locale, getBundleClassLoader()); } /** * Return a MessageFormat for the given bundle and code, fetching already * generated MessageFormats from the cache. * * @param bundle * the ResourceBundle to work on * @param code * the message code to retrieve * @param locale * the Locale to use to build the MessageFormat * @return the resulting MessageFormat, or <code>null</code> if no message * defined for the given code * @throws MissingResourceException * if thrown by the ResourceBundle */ protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) throws MissingResourceException { synchronized (this.cachedBundleMessageFormats) { Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats .get(bundle); Map<Locale, MessageFormat> localeMap = null; if (codeMap != null) { localeMap = codeMap.get(code); if (localeMap != null) { MessageFormat result = localeMap.get(locale); if (result != null) { return result; } } } String msg = getStringOrNull(bundle, code); if (msg != null) { if (codeMap == null) { codeMap = new HashMap<String, Map<Locale, MessageFormat>>(); this.cachedBundleMessageFormats.put(bundle, codeMap); } if (localeMap == null) { localeMap = new HashMap<Locale, MessageFormat>(); codeMap.put(code, localeMap); } MessageFormat result = createMessageFormat(msg, locale); localeMap.put(locale, result); return result; } return null; } } private String getStringOrNull(ResourceBundle bundle, String key) { try { return bundle.getString(key); } catch (MissingResourceException ex) { // Assume key not found // -> do NOT throw the exception to allow for checking parent // message source. return null; } } private ClassLoader getDefaultClassLoader() { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); } catch (Exception ex) { // Cannot access thread context ClassLoader - falling back to system // class loader... } if (cl == null) { // No thread context class loader -> use class loader of this class. cl = ResourceBundleI18nService.class.getClassLoader(); } return cl; } /** * Show the configuration of this MessageSource. */ @Override public String toString() { return getClass().getName() + ": basenames=" + ArrayUtils.toString(basenames) ; } }