001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.context.support;
018:
019: import java.text.MessageFormat;
020: import java.util.HashMap;
021: import java.util.Locale;
022: import java.util.Map;
023: import java.util.MissingResourceException;
024: import java.util.ResourceBundle;
025:
026: import org.springframework.beans.factory.BeanClassLoaderAware;
027: import org.springframework.util.Assert;
028: import org.springframework.util.ClassUtils;
029: import org.springframework.util.StringUtils;
030:
031: /**
032: * {@link org.springframework.context.MessageSource} implementation that
033: * accesses resource bundles using specified basenames. This class relies
034: * on the underlying JDK's {@link java.util.ResourceBundle} implementation,
035: * in combination with the JDK's standard message parsing provided by
036: * {@link java.text.MessageFormat}.
037: *
038: * <p>This MessageSource caches both the accessed ResourceBundle instances and
039: * the generated MessageFormats for each message. It also implements rendering of
040: * no-arg messages without MessageFormat, as supported by the AbstractMessageSource
041: * base class. The caching provided by this MessageSource is significantly faster
042: * than the built-in caching of the <code>java.util.ResourceBundle</code> class.
043: *
044: * <p>Unfortunately, <code>java.util.ResourceBundle</code> caches loaded bundles
045: * forever: Reloading a bundle during VM execution is <i>not</i> possible.
046: * As this MessageSource relies on ResourceBundle, it faces the same limitation.
047: * Consider {@link ReloadableResourceBundleMessageSource} for an alternative
048: * that is capable of refreshing the underlying bundle files.
049: *
050: * @author Rod Johnson
051: * @author Juergen Hoeller
052: * @see #setBasenames
053: * @see ReloadableResourceBundleMessageSource
054: * @see java.util.ResourceBundle
055: * @see java.text.MessageFormat
056: */
057: public class ResourceBundleMessageSource extends AbstractMessageSource
058: implements BeanClassLoaderAware {
059:
060: private String[] basenames = new String[0];
061:
062: private ClassLoader bundleClassLoader;
063:
064: private ClassLoader beanClassLoader = ClassUtils
065: .getDefaultClassLoader();
066:
067: /**
068: * Cache to hold loaded ResourceBundles.
069: * This Map is keyed with the bundle basename, which holds a Map that is
070: * keyed with the Locale and in turn holds the ResourceBundle instances.
071: * This allows for very efficient hash lookups, significantly faster
072: * than the ResourceBundle class's own cache.
073: */
074: private final Map cachedResourceBundles = new HashMap();
075:
076: /**
077: * Cache to hold already generated MessageFormats.
078: * This Map is keyed with the ResourceBundle, which holds a Map that is
079: * keyed with the message code, which in turn holds a Map that is keyed
080: * with the Locale and holds the MessageFormat values. This allows for
081: * very efficient hash lookups without concatenated keys.
082: * @see #getMessageFormat
083: */
084: private final Map cachedBundleMessageFormats = new HashMap();
085:
086: /**
087: * Set a single basename, following {@link java.util.ResourceBundle} conventions:
088: * essentially, a fully-qualified classpath location. If it doesn't contain a
089: * package qualifier (such as <code>org.mypackage</code>), it will be resolved
090: * from the classpath root.
091: * <p>Messages will normally be held in the "/lib" or "/classes" directory of
092: * a web application's WAR structure. They can also be held in jar files on
093: * the class path.
094: * <p>Note that ResourceBundle names are effectively classpath locations: As a
095: * consequence, the JDK's standard ResourceBundle treats dots as package separators.
096: * This means that "test.theme" is effectively equivalent to "test/theme",
097: * just like it is for programmatic <code>java.util.ResourceBundle</code> usage.
098: * @see #setBasenames
099: * @see java.util.ResourceBundle#getBundle(String)
100: */
101: public void setBasename(String basename) {
102: setBasenames(new String[] { basename });
103: }
104:
105: /**
106: * Set an array of basenames, each following {@link java.util.ResourceBundle}
107: * conventions: essentially, a fully-qualified classpath location. If it
108: * doesn't contain a package qualifier (such as <code>org.mypackage</code>),
109: * it will be resolved from the classpath root.
110: * <p>The associated resource bundles will be checked sequentially
111: * when resolving a message code. Note that message definitions in a
112: * <i>previous</i> resource bundle will override ones in a later bundle,
113: * due to the sequential lookup.
114: * <p>Note that ResourceBundle names are effectively classpath locations: As a
115: * consequence, the JDK's standard ResourceBundle treats dots as package separators.
116: * This means that "test.theme" is effectively equivalent to "test/theme",
117: * just like it is for programmatic <code>java.util.ResourceBundle</code> usage.
118: * @see #setBasename
119: * @see java.util.ResourceBundle#getBundle(String)
120: */
121: public void setBasenames(String[] basenames) {
122: if (basenames != null) {
123: this .basenames = new String[basenames.length];
124: for (int i = 0; i < basenames.length; i++) {
125: String basename = basenames[i];
126: Assert.hasText(basename, "Basename must not be empty");
127: this .basenames[i] = basename.trim();
128: }
129: } else {
130: this .basenames = new String[0];
131: }
132: }
133:
134: /**
135: * Set the ClassLoader to load resource bundles with.
136: * <p>Default is the containing BeanFactory's
137: * {@link org.springframework.beans.factory.BeanClassLoaderAware bean ClassLoader},
138: * or the default ClassLoader determined by
139: * {@link org.springframework.util.ClassUtils#getDefaultClassLoader()}
140: * if not running within a BeanFactory.
141: */
142: public void setBundleClassLoader(ClassLoader classLoader) {
143: this .bundleClassLoader = classLoader;
144: }
145:
146: /**
147: * Return the ClassLoader to load resource bundles with.
148: * <p>Default is the containing BeanFactory's bean ClassLoader.
149: * @see #setBundleClassLoader
150: */
151: protected ClassLoader getBundleClassLoader() {
152: return (this .bundleClassLoader != null ? this .bundleClassLoader
153: : this .beanClassLoader);
154: }
155:
156: public void setBeanClassLoader(ClassLoader classLoader) {
157: this .beanClassLoader = (classLoader != null ? classLoader
158: : ClassUtils.getDefaultClassLoader());
159: }
160:
161: /**
162: * Resolves the given message code as key in the registered resource bundles,
163: * returning the value found in the bundle as-is (without MessageFormat parsing).
164: */
165: protected String resolveCodeWithoutArguments(String code,
166: Locale locale) {
167: String result = null;
168: for (int i = 0; result == null && i < this .basenames.length; i++) {
169: ResourceBundle bundle = getResourceBundle(
170: this .basenames[i], locale);
171: if (bundle != null) {
172: result = getStringOrNull(bundle, code);
173: }
174: }
175: return result;
176: }
177:
178: /**
179: * Resolves the given message code as key in the registered resource bundles,
180: * using a cached MessageFormat instance per message code.
181: */
182: protected MessageFormat resolveCode(String code, Locale locale) {
183: MessageFormat messageFormat = null;
184: for (int i = 0; messageFormat == null
185: && i < this .basenames.length; i++) {
186: ResourceBundle bundle = getResourceBundle(
187: this .basenames[i], locale);
188: if (bundle != null) {
189: messageFormat = getMessageFormat(bundle, code, locale);
190: }
191: }
192: return messageFormat;
193: }
194:
195: /**
196: * Return a ResourceBundle for the given basename and code,
197: * fetching already generated MessageFormats from the cache.
198: * @param basename the basename of the ResourceBundle
199: * @param locale the Locale to find the ResourceBundle for
200: * @return the resulting ResourceBundle, or <code>null</code> if none
201: * found for the given basename and Locale
202: */
203: protected ResourceBundle getResourceBundle(String basename,
204: Locale locale) {
205: synchronized (this .cachedResourceBundles) {
206: Map localeMap = (Map) this .cachedResourceBundles
207: .get(basename);
208: if (localeMap != null) {
209: ResourceBundle bundle = (ResourceBundle) localeMap
210: .get(locale);
211: if (bundle != null) {
212: return bundle;
213: }
214: }
215: try {
216: ResourceBundle bundle = doGetBundle(basename, locale);
217: if (localeMap == null) {
218: localeMap = new HashMap();
219: this .cachedResourceBundles.put(basename, localeMap);
220: }
221: localeMap.put(locale, bundle);
222: return bundle;
223: } catch (MissingResourceException ex) {
224: if (logger.isWarnEnabled()) {
225: logger.warn("ResourceBundle [" + basename
226: + "] not found for MessageSource: "
227: + ex.getMessage());
228: }
229: // Assume bundle not found
230: // -> do NOT throw the exception to allow for checking parent message source.
231: return null;
232: }
233: }
234: }
235:
236: /**
237: * Obtain the resource bundle for the given basename and Locale.
238: * @param basename the basename to look for
239: * @param locale the Locale to look for
240: * @return the corresponding ResourceBundle
241: * @throws MissingResourceException if no matching bundle could be found
242: * @see java.util.ResourceBundle#getBundle(String, java.util.Locale, ClassLoader)
243: * @see #getBundleClassLoader()
244: */
245: protected ResourceBundle doGetBundle(String basename, Locale locale)
246: throws MissingResourceException {
247: return ResourceBundle.getBundle(basename, locale,
248: getBundleClassLoader());
249: }
250:
251: /**
252: * Return a MessageFormat for the given bundle and code,
253: * fetching already generated MessageFormats from the cache.
254: * @param bundle the ResourceBundle to work on
255: * @param code the message code to retrieve
256: * @param locale the Locale to use to build the MessageFormat
257: * @return the resulting MessageFormat, or <code>null</code> if no message
258: * defined for the given code
259: * @throws MissingResourceException if thrown by the ResourceBundle
260: */
261: protected MessageFormat getMessageFormat(ResourceBundle bundle,
262: String code, Locale locale) throws MissingResourceException {
263:
264: synchronized (this .cachedBundleMessageFormats) {
265: Map codeMap = (Map) this .cachedBundleMessageFormats
266: .get(bundle);
267: Map localeMap = null;
268: if (codeMap != null) {
269: localeMap = (Map) codeMap.get(code);
270: if (localeMap != null) {
271: MessageFormat result = (MessageFormat) localeMap
272: .get(locale);
273: if (result != null) {
274: return result;
275: }
276: }
277: }
278:
279: String msg = getStringOrNull(bundle, code);
280: if (msg != null) {
281: if (codeMap == null) {
282: codeMap = new HashMap();
283: this .cachedBundleMessageFormats
284: .put(bundle, codeMap);
285: }
286: if (localeMap == null) {
287: localeMap = new HashMap();
288: codeMap.put(code, localeMap);
289: }
290: MessageFormat result = createMessageFormat(msg, locale);
291: localeMap.put(locale, result);
292: return result;
293: }
294:
295: return null;
296: }
297: }
298:
299: private String getStringOrNull(ResourceBundle bundle, String key) {
300: try {
301: return bundle.getString(key);
302: } catch (MissingResourceException ex) {
303: // Assume key not found
304: // -> do NOT throw the exception to allow for checking parent message source.
305: return null;
306: }
307: }
308:
309: /**
310: * Show the configuration of this MessageSource.
311: */
312: public String toString() {
313: return getClass().getName()
314: + ": basenames=["
315: + StringUtils
316: .arrayToCommaDelimitedString(this .basenames)
317: + "]";
318: }
319:
320: }
|