/* * Copyright 2008 biaoping.yin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.frameworkset.spi.support; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.PropertyResourceBundle; import java.util.ResourceBundle; import org.frameworkset.spi.BeanClassLoaderAware; import org.frameworkset.util.Assert; import org.frameworkset.util.ClassUtils; import org.frameworkset.util.PropertiesPersister; import org.frameworkset.util.ResourceUtils; import org.frameworkset.util.io.DefaultResourceLoader; import org.frameworkset.util.io.Resource; import org.frameworkset.util.io.ResourceLoader; import com.frameworkset.util.DefaultPropertiesPersister; import com.frameworkset.util.SimpleStringUtil; /** * <p>Title: ResourceBundleMessageSource.java</p> * <p>Description: </p> * <p>bboss workgroup</p> * <p>Copyright (c) 2007</p> * @Date 2010-9-24 下午06:52:31 * @author biaoping.yin * @version 1.0 */ public class ResourceBundleMessageSource extends AbstractMessageSource implements BeanClassLoaderAware { private String[] basenames = new String[0]; private ClassLoader bundleClassLoader; private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private ResourceLoader resourceLoader = new DefaultResourceLoader(); private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); public ResourceBundleMessageSource() { super(); } /** * 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 cachedResourceBundles = new HashMap(); /** * 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 cachedBundleMessageFormats = new HashMap(); /** * 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 application'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[] basenames) { if (basenames != null) { this.basenames = new String[basenames.length]; for (int i = 0; i < basenames.length; i++) { String basename = basenames[i]; Assert.hasText(basename, "Basename must not be empty"); this.basenames[i] = basename.trim(); } } else { this.basenames = new String[0]; } } /** * Set the ClassLoader to load resource bundles with. * <p>Default is the containing BeanFactory's * or the default ClassLoader determined by * {@link 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.beanClassLoader); } public void setBeanClassLoader(ClassLoader classLoader) { this.beanClassLoader = (classLoader != null ? classLoader : ClassUtils.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). */ 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. */ 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 localeMap = (Map) this.cachedResourceBundles.get(basename); if (localeMap != null) { ResourceBundle bundle = (ResourceBundle) localeMap.get(locale); if (bundle != null) { return bundle; } } try { ResourceBundle bundle = doGetBundle(basename, locale); if (localeMap == null) { localeMap = new HashMap(); this.cachedResourceBundles.put(basename, localeMap); } localeMap.put(locale, bundle); return bundle; } catch (MissingResourceException ex) { { 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; ResourceBundle bundle = doGetDefaultBundle(basename); if (localeMap == null) { localeMap = new HashMap(); this.cachedResourceBundles.put(basename, localeMap); } localeMap.put(locale, bundle); return bundle; } } } /** * 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()); } private static final String PROPERTIES_SUFFIX = ".properties"; private static final String XML_SUFFIX = ".xml"; /** * Refresh the PropertiesHolder for the given bundle filename. * The holder can be <code>null</code> if not cached before, or a timed-out cache entry * (potentially getting re-validated against the current last-modified timestamp). * @param filename the bundle filename (basename + Locale) * @param propHolder the current PropertiesHolder for the bundle * */ protected ResourceBundle doGetDefaultBundle(String filename) { Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); if (!resource.exists()) { if(!filename.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { resource = this.resourceLoader.getResource(ResourceUtils.CLASSPATH_URL_PREFIX+ filename + PROPERTIES_SUFFIX); } } if (!resource.exists()) { resource = this.resourceLoader.getResource(filename + XML_SUFFIX); } if (!resource.exists()) { if(!filename.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) { resource = this.resourceLoader.getResource(ResourceUtils.CLASSPATH_URL_PREFIX+ filename + XML_SUFFIX); } } ResourceBundle propHolder = null; if (resource.exists()) { try { propHolder = loadProperties(resource, filename); } catch (IOException ex) { { logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex); } // Empty holder representing "not valid". // propHolder = new PropertyResourceBundle(null); } } else { // Resource does not exist. if (logger.isDebugEnabled()) { logger.debug("No properties file found for [" + filename + "] - neither plain properties nor XML"); } // Empty holder representing "not found". // propHolder = new PropertiesHolder(); } return propHolder; } /** * Load the properties from the given resource. * @param resource the resource to load from * @param filename the original bundle filename (basename + Locale) * @return the populated Properties instance * @throws IOException if properties loading failed */ protected ResourceBundle loadProperties(Resource resource, String filename) throws IOException { InputStream is = resource.getInputStream(); ResourceBundle ret = new PropertyResourceBundle(is); return ret; // try { // if (resource.getFilename().endsWith(XML_SUFFIX)) { // if (logger.isDebugEnabled()) { // logger.debug("Loading properties [" + resource.getFilename() + "]"); // } // this.propertiesPersister.loadFromXml(props, is); // } // else { // String encoding = null; // if (this.fileEncodings != null) { // encoding = this.fileEncodings.getProperty(filename); // } // if (encoding == null) { // encoding = this.defaultEncoding; // } // if (encoding != null) { // if (logger.isDebugEnabled()) { // logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); // } // this.propertiesPersister.load(props, new InputStreamReader(is, encoding)); // } // else { // if (logger.isDebugEnabled()) { // logger.debug("Loading properties [" + resource.getFilename() + "]"); // } // this.propertiesPersister.load(props, is); // } // } // return props; // } // finally { // is.close(); // } } /** * 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 codeMap = (Map) this.cachedBundleMessageFormats.get(bundle); Map localeMap = null; if (codeMap != null) { localeMap = (Map) codeMap.get(code); if (localeMap != null) { MessageFormat result = (MessageFormat) localeMap.get(locale); if (result != null) { return result; } } } String msg = getStringOrNull(bundle, code); if (msg != null) { if (codeMap == null) { codeMap = new HashMap(); this.cachedBundleMessageFormats.put(bundle, codeMap); } if (localeMap == null) { localeMap = new HashMap(); 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; } } /** * Show the configuration of this MessageSource. */ public String toString() { return getClass().getName() + ": basenames=[" + SimpleStringUtil.arrayToCommaDelimitedString(this.basenames) + "]"; } }