001/*
002 * Copyright 2002-2018 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.ui.context.support;
018
019import java.util.Map;
020import java.util.concurrent.ConcurrentHashMap;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024
025import org.springframework.beans.factory.BeanClassLoaderAware;
026import org.springframework.context.HierarchicalMessageSource;
027import org.springframework.context.MessageSource;
028import org.springframework.context.support.ResourceBundleMessageSource;
029import org.springframework.lang.Nullable;
030import org.springframework.ui.context.HierarchicalThemeSource;
031import org.springframework.ui.context.Theme;
032import org.springframework.ui.context.ThemeSource;
033
034/**
035 * {@link ThemeSource} implementation that looks up an individual
036 * {@link java.util.ResourceBundle} per theme. The theme name gets
037 * interpreted as ResourceBundle basename, supporting a common
038 * basename prefix for all themes.
039 *
040 * @author Jean-Pierre Pawlak
041 * @author Juergen Hoeller
042 * @see #setBasenamePrefix
043 * @see java.util.ResourceBundle
044 * @see org.springframework.context.support.ResourceBundleMessageSource
045 */
046public class ResourceBundleThemeSource implements HierarchicalThemeSource, BeanClassLoaderAware {
047
048        protected final Log logger = LogFactory.getLog(getClass());
049
050        @Nullable
051        private ThemeSource parentThemeSource;
052
053        private String basenamePrefix = "";
054
055        @Nullable
056        private String defaultEncoding;
057
058        @Nullable
059        private Boolean fallbackToSystemLocale;
060
061        @Nullable
062        private ClassLoader beanClassLoader;
063
064        /** Map from theme name to Theme instance. */
065        private final Map<String, Theme> themeCache = new ConcurrentHashMap<>();
066
067
068        @Override
069        public void setParentThemeSource(@Nullable ThemeSource parent) {
070                this.parentThemeSource = parent;
071
072                // Update existing Theme objects.
073                // Usually there shouldn't be any at the time of this call.
074                synchronized (this.themeCache) {
075                        for (Theme theme : this.themeCache.values()) {
076                                initParent(theme);
077                        }
078                }
079        }
080
081        @Override
082        @Nullable
083        public ThemeSource getParentThemeSource() {
084                return this.parentThemeSource;
085        }
086
087        /**
088         * Set the prefix that gets applied to the ResourceBundle basenames,
089         * i.e. the theme names.
090         * E.g.: basenamePrefix="test.", themeName="theme" -> basename="test.theme".
091         * <p>Note that ResourceBundle names are effectively classpath locations: As a
092         * consequence, the JDK's standard ResourceBundle treats dots as package separators.
093         * This means that "test.theme" is effectively equivalent to "test/theme",
094         * just like it is for programmatic {@code java.util.ResourceBundle} usage.
095         * @see java.util.ResourceBundle#getBundle(String)
096         */
097        public void setBasenamePrefix(@Nullable String basenamePrefix) {
098                this.basenamePrefix = (basenamePrefix != null ? basenamePrefix : "");
099        }
100
101        /**
102         * Set the default charset to use for parsing resource bundle files.
103         * <p>{@link ResourceBundleMessageSource}'s default is the
104         * {@code java.util.ResourceBundle} default encoding: ISO-8859-1.
105         * @since 4.2
106         * @see ResourceBundleMessageSource#setDefaultEncoding
107         */
108        public void setDefaultEncoding(@Nullable String defaultEncoding) {
109                this.defaultEncoding = defaultEncoding;
110        }
111
112        /**
113         * Set whether to fall back to the system Locale if no files for a
114         * specific Locale have been found.
115         * <p>{@link ResourceBundleMessageSource}'s default is "true".
116         * @since 4.2
117         * @see ResourceBundleMessageSource#setFallbackToSystemLocale
118         */
119        public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) {
120                this.fallbackToSystemLocale = fallbackToSystemLocale;
121        }
122
123        @Override
124        public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) {
125                this.beanClassLoader = beanClassLoader;
126        }
127
128
129        /**
130         * This implementation returns a SimpleTheme instance, holding a
131         * ResourceBundle-based MessageSource whose basename corresponds to
132         * the given theme name (prefixed by the configured "basenamePrefix").
133         * <p>SimpleTheme instances are cached per theme name. Use a reloadable
134         * MessageSource if themes should reflect changes to the underlying files.
135         * @see #setBasenamePrefix
136         * @see #createMessageSource
137         */
138        @Override
139        @Nullable
140        public Theme getTheme(String themeName) {
141                Theme theme = this.themeCache.get(themeName);
142                if (theme == null) {
143                        synchronized (this.themeCache) {
144                                theme = this.themeCache.get(themeName);
145                                if (theme == null) {
146                                        String basename = this.basenamePrefix + themeName;
147                                        MessageSource messageSource = createMessageSource(basename);
148                                        theme = new SimpleTheme(themeName, messageSource);
149                                        initParent(theme);
150                                        this.themeCache.put(themeName, theme);
151                                        if (logger.isDebugEnabled()) {
152                                                logger.debug("Theme created: name '" + themeName + "', basename [" + basename + "]");
153                                        }
154                                }
155                        }
156                }
157                return theme;
158        }
159
160        /**
161         * Create a MessageSource for the given basename,
162         * to be used as MessageSource for the corresponding theme.
163         * <p>Default implementation creates a ResourceBundleMessageSource.
164         * for the given basename. A subclass could create a specifically
165         * configured ReloadableResourceBundleMessageSource, for example.
166         * @param basename the basename to create a MessageSource for
167         * @return the MessageSource
168         * @see org.springframework.context.support.ResourceBundleMessageSource
169         * @see org.springframework.context.support.ReloadableResourceBundleMessageSource
170         */
171        protected MessageSource createMessageSource(String basename) {
172                ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
173                messageSource.setBasename(basename);
174                if (this.defaultEncoding != null) {
175                        messageSource.setDefaultEncoding(this.defaultEncoding);
176                }
177                if (this.fallbackToSystemLocale != null) {
178                        messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale);
179                }
180                if (this.beanClassLoader != null) {
181                        messageSource.setBeanClassLoader(this.beanClassLoader);
182                }
183                return messageSource;
184        }
185
186        /**
187         * Initialize the MessageSource of the given theme with the
188         * one from the corresponding parent of this ThemeSource.
189         * @param theme the Theme to (re-)initialize
190         */
191        protected void initParent(Theme theme) {
192                if (theme.getMessageSource() instanceof HierarchicalMessageSource) {
193                        HierarchicalMessageSource messageSource = (HierarchicalMessageSource) theme.getMessageSource();
194                        if (getParentThemeSource() != null && messageSource.getParentMessageSource() == null) {
195                                Theme parentTheme = getParentThemeSource().getTheme(theme.getName());
196                                if (parentTheme != null) {
197                                        messageSource.setParentMessageSource(parentTheme.getMessageSource());
198                                }
199                        }
200                }
201        }
202
203}