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}