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