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.io.File;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.InputStreamReader;
023: import java.text.MessageFormat;
024: import java.util.ArrayList;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Locale;
029: import java.util.Map;
030: import java.util.Properties;
031:
032: import org.springframework.context.ResourceLoaderAware;
033: import org.springframework.core.io.DefaultResourceLoader;
034: import org.springframework.core.io.Resource;
035: import org.springframework.core.io.ResourceLoader;
036: import org.springframework.util.Assert;
037: import org.springframework.util.DefaultPropertiesPersister;
038: import org.springframework.util.PropertiesPersister;
039: import org.springframework.util.StringUtils;
040:
041: /**
042: * {@link org.springframework.context.MessageSource} implementation that
043: * accesses resource bundles using specified basenames. This class uses
044: * {@link java.util.Properties} instances as its custom data structure for
045: * messages, loading them via a {@link org.springframework.util.PropertiesPersister}
046: * strategy: The default strategy is capable of loading properties files
047: * with a specific character encoding, if desired.
048: *
049: * <p>In contrast to {@link ResourceBundleMessageSource}, this class supports
050: * reloading of properties files through the {@link #setCacheSeconds "cacheSeconds"}
051: * setting, and also through programmatically clearing the properties cache.
052: * Since application servers typically cache all files loaded from the classpath,
053: * it is necessary to store resources somewhere else (for example, in the
054: * "WEB-INF" directory of a web app). Otherwise changes of files in the
055: * classpath will <i>not</i> be reflected in the application.
056: *
057: * <p>Note that the base names set as {@link #setBasenames "basenames"} property
058: * are treated in a slightly different fashion than the "basenames" property of
059: * {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not
060: * specifying file extension or language codes, but can refer to any Spring resource
061: * location (instead of being restricted to classpath resources). With a "classpath:"
062: * prefix, resources can still be loaded from the classpath, but "cacheSeconds" values
063: * other than "-1" (caching forever) will not work in this case.
064: *
065: * <p>This MessageSource implementation is usually slightly faster than
066: * {@link ResourceBundleMessageSource}, which builds on {@link java.util.ResourceBundle}
067: * - in the default mode, i.e. when caching forever. With "cacheSeconds" set to 1,
068: * message lookup takes about twice as long - with the benefit that changes in
069: * individual properties files are detected with a maximum delay of 1 second.
070: * Higher "cacheSeconds" values usually <i>do not</i> make a significant difference.
071: *
072: * <p>This MessageSource can easily be used outside of an
073: * {@link org.springframework.context.ApplicationContext}: It will use a
074: * {@link org.springframework.core.io.DefaultResourceLoader} as default,
075: * simply getting overridden with the ApplicationContext's resource loader
076: * if running in a context. It does not have any other specific dependencies.
077: *
078: * <p>Thanks to Thomas Achleitner for providing the initial implementation of
079: * this message source!
080: *
081: * @author Juergen Hoeller
082: * @see #setCacheSeconds
083: * @see #setBasenames
084: * @see #setDefaultEncoding
085: * @see #setFileEncodings
086: * @see #setPropertiesPersister
087: * @see #setResourceLoader
088: * @see org.springframework.util.DefaultPropertiesPersister
089: * @see org.springframework.core.io.DefaultResourceLoader
090: * @see ResourceBundleMessageSource
091: * @see java.util.ResourceBundle
092: */
093: public class ReloadableResourceBundleMessageSource extends
094: AbstractMessageSource implements ResourceLoaderAware {
095:
096: private static final String PROPERTIES_SUFFIX = ".properties";
097:
098: private static final String XML_SUFFIX = ".xml";
099:
100: private String[] basenames = new String[0];
101:
102: private String defaultEncoding;
103:
104: private Properties fileEncodings;
105:
106: private boolean fallbackToSystemLocale = true;
107:
108: private long cacheMillis = -1;
109:
110: private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
111:
112: private ResourceLoader resourceLoader = new DefaultResourceLoader();
113:
114: /** Cache to hold filename lists per Locale */
115: private final Map cachedFilenames = new HashMap();
116:
117: /** Cache to hold already loaded properties per filename */
118: private final Map cachedProperties = new HashMap();
119:
120: /** Cache to hold merged loaded properties per basename */
121: private final Map cachedMergedProperties = new HashMap();
122:
123: /**
124: * Set a single basename, following the basic ResourceBundle convention of
125: * not specifying file extension or language codes, but in contrast to
126: * {@link ResourceBundleMessageSource} referring to a Spring resource location:
127: * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties",
128: * "WEB-INF/messages_en.properties", etc.
129: * <p>As of Spring 1.2.2, XML properties files are also supported:
130: * e.g. "WEB-INF/messages" will find and load "WEB-INF/messages.xml",
131: * "WEB-INF/messages_en.xml", etc as well. Note that this will only
132: * work on JDK 1.5+.
133: * @param basename the single basename
134: * @see #setBasenames
135: * @see org.springframework.core.io.ResourceEditor
136: * @see java.util.ResourceBundle
137: */
138: public void setBasename(String basename) {
139: setBasenames(new String[] { basename });
140: }
141:
142: /**
143: * Set an array of basenames, each following the basic ResourceBundle convention
144: * of not specifying file extension or language codes, but in contrast to
145: * {@link ResourceBundleMessageSource} referring to a Spring resource location:
146: * e.g. "WEB-INF/messages" for "WEB-INF/messages.properties",
147: * "WEB-INF/messages_en.properties", etc.
148: * <p>As of Spring 1.2.2, XML properties files are also supported:
149: * e.g. "WEB-INF/messages" will find and load "WEB-INF/messages.xml",
150: * "WEB-INF/messages_en.xml", etc as well. Note that this will only
151: * work on JDK 1.5+.
152: * <p>The associated resource bundles will be checked sequentially
153: * when resolving a message code. Note that message definitions in a
154: * <i>previous</i> resource bundle will override ones in a later bundle,
155: * due to the sequential lookup.
156: * @param basenames an array of basenames
157: * @see #setBasename
158: * @see java.util.ResourceBundle
159: */
160: public void setBasenames(String[] basenames) {
161: if (basenames != null) {
162: this .basenames = new String[basenames.length];
163: for (int i = 0; i < basenames.length; i++) {
164: String basename = basenames[i];
165: Assert.hasText(basename, "Basename must not be empty");
166: this .basenames[i] = basename.trim();
167: }
168: } else {
169: this .basenames = new String[0];
170: }
171: }
172:
173: /**
174: * Set the default charset to use for parsing properties files.
175: * Used if no file-specific charset is specified for a file.
176: * <p>Default is none, using the <code>java.util.Properties</code>
177: * default encoding.
178: * <p>Only applies to classic properties files, not to XML files.
179: * @param defaultEncoding the default charset
180: * @see #setFileEncodings
181: * @see org.springframework.util.PropertiesPersister#load
182: */
183: public void setDefaultEncoding(String defaultEncoding) {
184: this .defaultEncoding = defaultEncoding;
185: }
186:
187: /**
188: * Set per-file charsets to use for parsing properties files.
189: * <p>Only applies to classic properties files, not to XML files.
190: * @param fileEncodings Properties with filenames as keys and charset
191: * names as values. Filenames have to match the basename syntax,
192: * with optional locale-specific appendices: e.g. "WEB-INF/messages"
193: * or "WEB-INF/messages_en".
194: * @see #setBasenames
195: * @see org.springframework.util.PropertiesPersister#load
196: */
197: public void setFileEncodings(Properties fileEncodings) {
198: this .fileEncodings = fileEncodings;
199: }
200:
201: /**
202: * Set whether to fall back to the system Locale if no files for a specific
203: * Locale have been found. Default is "true"; if this is turned off, the only
204: * fallback will be the default file (e.g. "messages.properties" for
205: * basename "messages").
206: * <p>Falling back to the system Locale is the default behavior of
207: * <code>java.util.ResourceBundle</code>. However, this is often not
208: * desirable in an application server environment, where the system Locale
209: * is not relevant to the application at all: Set this flag to "false"
210: * in such a scenario.
211: */
212: public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
213: this .fallbackToSystemLocale = fallbackToSystemLocale;
214: }
215:
216: /**
217: * Set the number of seconds to cache loaded properties files.
218: * <ul>
219: * <li>Default is "-1", indicating to cache forever (just like
220: * <code>java.util.ResourceBundle</code>).
221: * <li>A positive number will cache loaded properties files for the given
222: * number of seconds. This is essentially the interval between refresh checks.
223: * Note that a refresh attempt will first check the last-modified timestamp
224: * of the file before actually reloading it; so if files don't change, this
225: * interval can be set rather low, as refresh attempts will not actually reload.
226: * <li>A value of "0" will check the last-modified timestamp of the file on
227: * every message access. <b>Do not use this in a production environment!</b>
228: * </ul>
229: */
230: public void setCacheSeconds(int cacheSeconds) {
231: this .cacheMillis = (cacheSeconds * 1000);
232: }
233:
234: /**
235: * Set the PropertiesPersister to use for parsing properties files.
236: * <p>The default is a DefaultPropertiesPersister.
237: * @see org.springframework.util.DefaultPropertiesPersister
238: */
239: public void setPropertiesPersister(
240: PropertiesPersister propertiesPersister) {
241: this .propertiesPersister = (propertiesPersister != null ? propertiesPersister
242: : new DefaultPropertiesPersister());
243: }
244:
245: /**
246: * Set the ResourceLoader to use for loading bundle properties files.
247: * <p>The default is a DefaultResourceLoader. Will get overridden by the
248: * ApplicationContext if running in a context, as it implements the
249: * ResourceLoaderAware interface. Can be manually overridden when
250: * running outside of an ApplicationContext.
251: * @see org.springframework.core.io.DefaultResourceLoader
252: * @see org.springframework.context.ResourceLoaderAware
253: */
254: public void setResourceLoader(ResourceLoader resourceLoader) {
255: this .resourceLoader = (resourceLoader != null ? resourceLoader
256: : new DefaultResourceLoader());
257: }
258:
259: /**
260: * Resolves the given message code as key in the retrieved bundle files,
261: * returning the value found in the bundle as-is (without MessageFormat parsing).
262: */
263: protected String resolveCodeWithoutArguments(String code,
264: Locale locale) {
265: if (this .cacheMillis < 0) {
266: PropertiesHolder propHolder = getMergedProperties(locale);
267: String result = propHolder.getProperty(code);
268: if (result != null) {
269: return result;
270: }
271: } else {
272: for (int i = 0; i < this .basenames.length; i++) {
273: List filenames = calculateAllFilenames(
274: this .basenames[i], locale);
275: for (int j = 0; j < filenames.size(); j++) {
276: String filename = (String) filenames.get(j);
277: PropertiesHolder propHolder = getProperties(filename);
278: String result = propHolder.getProperty(code);
279: if (result != null) {
280: return result;
281: }
282: }
283: }
284: }
285: return null;
286: }
287:
288: /**
289: * Resolves the given message code as key in the retrieved bundle files,
290: * using a cached MessageFormat instance per message code.
291: */
292: protected MessageFormat resolveCode(String code, Locale locale) {
293: if (this .cacheMillis < 0) {
294: PropertiesHolder propHolder = getMergedProperties(locale);
295: MessageFormat result = propHolder.getMessageFormat(code,
296: locale);
297: if (result != null) {
298: return result;
299: }
300: } else {
301: for (int i = 0; i < this .basenames.length; i++) {
302: List filenames = calculateAllFilenames(
303: this .basenames[i], locale);
304: for (int j = 0; j < filenames.size(); j++) {
305: String filename = (String) filenames.get(j);
306: PropertiesHolder propHolder = getProperties(filename);
307: MessageFormat result = propHolder.getMessageFormat(
308: code, locale);
309: if (result != null) {
310: return result;
311: }
312: }
313: }
314: }
315: return null;
316: }
317:
318: /**
319: * Get a PropertiesHolder that contains the actually visible properties
320: * for a Locale, after merging all specified resource bundles.
321: * Either fetches the holder from the cache or freshly loads it.
322: * <p>Only used when caching resource bundle contents forever, i.e.
323: * with cacheSeconds < 0. Therefore, merged properties are always
324: * cached forever.
325: */
326: protected PropertiesHolder getMergedProperties(Locale locale) {
327: synchronized (this .cachedMergedProperties) {
328: PropertiesHolder mergedHolder = (PropertiesHolder) this .cachedMergedProperties
329: .get(locale);
330: if (mergedHolder != null) {
331: return mergedHolder;
332: }
333: Properties mergedProps = new Properties();
334: mergedHolder = new PropertiesHolder(mergedProps, -1);
335: for (int i = this .basenames.length - 1; i >= 0; i--) {
336: List filenames = calculateAllFilenames(
337: this .basenames[i], locale);
338: for (int j = filenames.size() - 1; j >= 0; j--) {
339: String filename = (String) filenames.get(j);
340: PropertiesHolder propHolder = getProperties(filename);
341: if (propHolder.getProperties() != null) {
342: mergedProps.putAll(propHolder.getProperties());
343: }
344: }
345: }
346: this .cachedMergedProperties.put(locale, mergedHolder);
347: return mergedHolder;
348: }
349: }
350:
351: /**
352: * Calculate all filenames for the given bundle basename and Locale.
353: * Will calculate filenames for the given Locale, the system Locale
354: * (if applicable), and the default file.
355: * @param basename the basename of the bundle
356: * @param locale the locale
357: * @return the List of filenames to check
358: * @see #setFallbackToSystemLocale
359: * @see #calculateFilenamesForLocale
360: */
361: protected List calculateAllFilenames(String basename, Locale locale) {
362: synchronized (this .cachedFilenames) {
363: Map localeMap = (Map) this .cachedFilenames.get(basename);
364: if (localeMap != null) {
365: List filenames = (List) localeMap.get(locale);
366: if (filenames != null) {
367: return filenames;
368: }
369: }
370: List filenames = new ArrayList(7);
371: filenames.addAll(calculateFilenamesForLocale(basename,
372: locale));
373: if (this .fallbackToSystemLocale
374: && !locale.equals(Locale.getDefault())) {
375: List fallbackFilenames = calculateFilenamesForLocale(
376: basename, Locale.getDefault());
377: for (Iterator it = fallbackFilenames.iterator(); it
378: .hasNext();) {
379: String fallbackFilename = (String) it.next();
380: if (!filenames.contains(fallbackFilename)) {
381: // Entry for fallback locale that isn't already in filenames list.
382: filenames.add(fallbackFilename);
383: }
384: }
385: }
386: filenames.add(basename);
387: if (localeMap != null) {
388: localeMap.put(locale, filenames);
389: } else {
390: localeMap = new HashMap();
391: localeMap.put(locale, filenames);
392: this .cachedFilenames.put(basename, localeMap);
393: }
394: return filenames;
395: }
396: }
397:
398: /**
399: * Calculate the filenames for the given bundle basename and Locale,
400: * appending language code, country code, and variant code.
401: * E.g.: basename "messages", Locale "de_AT_oo" -> "messages_de_AT_OO",
402: * "messages_de_AT", "messages_de".
403: * @param basename the basename of the bundle
404: * @param locale the locale
405: * @return the List of filenames to check
406: */
407: protected List calculateFilenamesForLocale(String basename,
408: Locale locale) {
409: List result = new ArrayList(3);
410: String language = locale.getLanguage();
411: String country = locale.getCountry();
412: String variant = locale.getVariant();
413: StringBuffer temp = new StringBuffer(basename);
414:
415: if (language.length() > 0) {
416: temp.append('_').append(language);
417: result.add(0, temp.toString());
418: }
419:
420: if (country.length() > 0) {
421: temp.append('_').append(country);
422: result.add(0, temp.toString());
423: }
424:
425: if (variant.length() > 0) {
426: temp.append('_').append(variant);
427: result.add(0, temp.toString());
428: }
429:
430: return result;
431: }
432:
433: /**
434: * Get a PropertiesHolder for the given filename, either from the
435: * cache or freshly loaded.
436: * @param filename the bundle filename (basename + Locale)
437: * @return the current PropertiesHolder for the bundle
438: */
439: protected PropertiesHolder getProperties(String filename) {
440: synchronized (this .cachedProperties) {
441: PropertiesHolder propHolder = (PropertiesHolder) this .cachedProperties
442: .get(filename);
443: if (propHolder != null
444: && (propHolder.getRefreshTimestamp() < 0 || propHolder
445: .getRefreshTimestamp() > System
446: .currentTimeMillis()
447: - this .cacheMillis)) {
448: // up to date
449: return propHolder;
450: }
451: return refreshProperties(filename, propHolder);
452: }
453: }
454:
455: /**
456: * Refresh the PropertiesHolder for the given bundle filename.
457: * The holder can be <code>null</code> if not cached before, or a timed-out cache entry
458: * (potentially getting re-validated against the current last-modified timestamp).
459: * @param filename the bundle filename (basename + Locale)
460: * @param propHolder the current PropertiesHolder for the bundle
461: */
462: protected PropertiesHolder refreshProperties(String filename,
463: PropertiesHolder propHolder) {
464: long refreshTimestamp = (this .cacheMillis < 0) ? -1 : System
465: .currentTimeMillis();
466:
467: Resource resource = this .resourceLoader.getResource(filename
468: + PROPERTIES_SUFFIX);
469: if (!resource.exists()) {
470: resource = this .resourceLoader.getResource(filename
471: + XML_SUFFIX);
472: }
473:
474: if (resource.exists()) {
475: try {
476: long fileTimestamp = -1;
477:
478: if (this .cacheMillis >= 0) {
479: // Last-modified timestamp of file will just be read if caching with timeout.
480: File file = null;
481: try {
482: file = resource.getFile();
483: } catch (IOException ex) {
484: // Probably a class path resource: cache it forever.
485: if (logger.isDebugEnabled()) {
486: logger
487: .debug(
488: resource
489: + " could not be resolved in the file system - assuming that is hasn't changed",
490: ex);
491: }
492: file = null;
493: }
494: if (file != null) {
495: fileTimestamp = file.lastModified();
496: if (fileTimestamp == 0) {
497: throw new IOException("File ["
498: + file.getAbsolutePath()
499: + "] does not exist");
500: }
501: if (propHolder != null
502: && propHolder.getFileTimestamp() == fileTimestamp) {
503: if (logger.isDebugEnabled()) {
504: logger
505: .debug("Re-caching properties for filename ["
506: + filename
507: + "] - file hasn't been modified");
508: }
509: propHolder
510: .setRefreshTimestamp(refreshTimestamp);
511: return propHolder;
512: }
513: }
514: }
515:
516: Properties props = loadProperties(resource, filename);
517: propHolder = new PropertiesHolder(props, fileTimestamp);
518: }
519:
520: catch (IOException ex) {
521: if (logger.isWarnEnabled()) {
522: logger.warn("Could not parse properties file ["
523: + resource.getFilename() + "]: "
524: + ex.getMessage(), ex);
525: }
526: // Empty holder representing "not valid".
527: propHolder = new PropertiesHolder();
528: }
529: }
530:
531: else {
532: // Resource does not exist.
533: if (logger.isDebugEnabled()) {
534: logger.debug("No properties file found for ["
535: + filename
536: + "] - neither plain properties nor XML");
537: }
538: // Empty holder representing "not found".
539: propHolder = new PropertiesHolder();
540: }
541:
542: propHolder.setRefreshTimestamp(refreshTimestamp);
543: this .cachedProperties.put(filename, propHolder);
544: return propHolder;
545: }
546:
547: /**
548: * Load the properties from the given resource.
549: * @param resource the resource to load from
550: * @param filename the original bundle filename (basename + Locale)
551: * @return the populated Properties instance
552: * @throws IOException if properties loading failed
553: */
554: protected Properties loadProperties(Resource resource,
555: String filename) throws IOException {
556: InputStream is = resource.getInputStream();
557: Properties props = new Properties();
558: try {
559: if (resource.getFilename().endsWith(XML_SUFFIX)) {
560: if (logger.isDebugEnabled()) {
561: logger.debug("Loading properties ["
562: + resource.getFilename() + "]");
563: }
564: this .propertiesPersister.loadFromXml(props, is);
565: } else {
566: String encoding = null;
567: if (this .fileEncodings != null) {
568: encoding = this .fileEncodings.getProperty(filename);
569: }
570: if (encoding == null) {
571: encoding = this .defaultEncoding;
572: }
573: if (encoding != null) {
574: if (logger.isDebugEnabled()) {
575: logger.debug("Loading properties ["
576: + resource.getFilename()
577: + "] with encoding '" + encoding + "'");
578: }
579: this .propertiesPersister.load(props,
580: new InputStreamReader(is, encoding));
581: } else {
582: if (logger.isDebugEnabled()) {
583: logger.debug("Loading properties ["
584: + resource.getFilename() + "]");
585: }
586: this .propertiesPersister.load(props, is);
587: }
588: }
589: return props;
590: } finally {
591: is.close();
592: }
593: }
594:
595: /**
596: * Clear the resource bundle cache.
597: * Subsequent resolve calls will lead to reloading of the properties files.
598: */
599: public void clearCache() {
600: logger.debug("Clearing entire resource bundle cache");
601: synchronized (this .cachedProperties) {
602: this .cachedProperties.clear();
603: }
604: synchronized (this .cachedMergedProperties) {
605: this .cachedMergedProperties.clear();
606: }
607: }
608:
609: /**
610: * Clear the resource bundle caches of this MessageSource and all its ancestors.
611: * @see #clearCache
612: */
613: public void clearCacheIncludingAncestors() {
614: clearCache();
615: if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource) {
616: ((ReloadableResourceBundleMessageSource) getParentMessageSource())
617: .clearCacheIncludingAncestors();
618: }
619: }
620:
621: public String toString() {
622: return getClass().getName()
623: + ": basenames=["
624: + StringUtils
625: .arrayToCommaDelimitedString(this .basenames)
626: + "]";
627: }
628:
629: /**
630: * PropertiesHolder for caching.
631: * Stores the last-modified timestamp of the source file for efficient
632: * change detection, and the timestamp of the last refresh attempt
633: * (updated every time the cache entry gets re-validated).
634: */
635: protected class PropertiesHolder {
636:
637: private Properties properties;
638:
639: private long fileTimestamp = -1;
640:
641: private long refreshTimestamp = -1;
642:
643: /** Cache to hold already generated MessageFormats per message code */
644: private final Map cachedMessageFormats = new HashMap();
645:
646: public PropertiesHolder(Properties properties,
647: long fileTimestamp) {
648: this .properties = properties;
649: this .fileTimestamp = fileTimestamp;
650: }
651:
652: public PropertiesHolder() {
653: }
654:
655: public Properties getProperties() {
656: return properties;
657: }
658:
659: public long getFileTimestamp() {
660: return fileTimestamp;
661: }
662:
663: public void setRefreshTimestamp(long refreshTimestamp) {
664: this .refreshTimestamp = refreshTimestamp;
665: }
666:
667: public long getRefreshTimestamp() {
668: return refreshTimestamp;
669: }
670:
671: public String getProperty(String code) {
672: if (this .properties == null) {
673: return null;
674: }
675: return this .properties.getProperty(code);
676: }
677:
678: public MessageFormat getMessageFormat(String code, Locale locale) {
679: if (this .properties == null) {
680: return null;
681: }
682: synchronized (this .cachedMessageFormats) {
683: Map localeMap = (Map) this .cachedMessageFormats
684: .get(code);
685: if (localeMap != null) {
686: MessageFormat result = (MessageFormat) localeMap
687: .get(locale);
688: if (result != null) {
689: return result;
690: }
691: }
692: String msg = this .properties.getProperty(code);
693: if (msg != null) {
694: if (localeMap == null) {
695: localeMap = new HashMap();
696: this .cachedMessageFormats.put(code, localeMap);
697: }
698: MessageFormat result = createMessageFormat(msg,
699: locale);
700: localeMap.put(locale, result);
701: return result;
702: }
703: return null;
704: }
705: }
706: }
707:
708: }
|