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.web.servlet.view; 018 019import java.util.HashMap; 020import java.util.LinkedList; 021import java.util.List; 022import java.util.Locale; 023import java.util.Map; 024import java.util.MissingResourceException; 025import java.util.ResourceBundle; 026 027import org.springframework.beans.BeansException; 028import org.springframework.beans.factory.BeanFactory; 029import org.springframework.beans.factory.DisposableBean; 030import org.springframework.beans.factory.InitializingBean; 031import org.springframework.beans.factory.NoSuchBeanDefinitionException; 032import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader; 033import org.springframework.context.ConfigurableApplicationContext; 034import org.springframework.core.Ordered; 035import org.springframework.web.context.support.GenericWebApplicationContext; 036import org.springframework.web.servlet.View; 037 038/** 039 * A {@link org.springframework.web.servlet.ViewResolver} implementation that uses 040 * bean definitions in a {@link ResourceBundle}, specified by the bundle basename. 041 * 042 * <p>The bundle is typically defined in a properties file, located in the classpath. 043 * The default bundle basename is "views". 044 * 045 * <p>This {@code ViewResolver} supports localized view definitions, using the 046 * default support of {@link java.util.PropertyResourceBundle}. For example, the 047 * basename "views" will be resolved as class path resources "views_de_AT.properties", 048 * "views_de.properties", "views.properties" - for a given Locale "de_AT". 049 * 050 * <p>Note: This {@code ViewResolver} implements the {@link Ordered} interface 051 * in order to allow for flexible participation in {@code ViewResolver} chaining. 052 * For example, some special views could be defined via this {@code ViewResolver} 053 * (giving it 0 as "order" value), while all remaining views could be resolved by 054 * a {@link UrlBasedViewResolver}. 055 * 056 * @author Rod Johnson 057 * @author Juergen Hoeller 058 * @see java.util.ResourceBundle#getBundle 059 * @see java.util.PropertyResourceBundle 060 * @see UrlBasedViewResolver 061 */ 062public class ResourceBundleViewResolver extends AbstractCachingViewResolver 063 implements Ordered, InitializingBean, DisposableBean { 064 065 /** The default basename if no other basename is supplied */ 066 public static final String DEFAULT_BASENAME = "views"; 067 068 069 private String[] basenames = new String[] {DEFAULT_BASENAME}; 070 071 private ClassLoader bundleClassLoader = Thread.currentThread().getContextClassLoader(); 072 073 private String defaultParentView; 074 075 private Locale[] localesToInitialize; 076 077 private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered 078 079 /* Locale -> BeanFactory */ 080 private final Map<Locale, BeanFactory> localeCache = 081 new HashMap<Locale, BeanFactory>(); 082 083 /* List of ResourceBundle -> BeanFactory */ 084 private final Map<List<ResourceBundle>, ConfigurableApplicationContext> bundleCache = 085 new HashMap<List<ResourceBundle>, ConfigurableApplicationContext>(); 086 087 088 /** 089 * Set a single basename, following {@link java.util.ResourceBundle} conventions. 090 * The default is "views". 091 * <p>{@code ResourceBundle} supports different locale suffixes. For example, 092 * a base name of "views" might map to {@code ResourceBundle} files 093 * "views", "views_en_au" and "views_de". 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} usage. 098 * @see #setBasenames 099 * @see ResourceBundle#getBundle(String) 100 * @see ResourceBundle#getBundle(String, Locale) 101 */ 102 public void setBasename(String basename) { 103 setBasenames(basename); 104 } 105 106 /** 107 * Set an array of basenames, each following {@link java.util.ResourceBundle} 108 * conventions. The default is a single basename "views". 109 * <p>{@code ResourceBundle} supports different locale suffixes. For example, 110 * a base name of "views" might map to {@code ResourceBundle} files 111 * "views", "views_en_au" and "views_de". 112 * <p>The associated resource bundles will be checked sequentially when resolving 113 * a message code. Note that message definitions in a <i>previous</i> resource 114 * bundle will override ones in a later bundle, due to the sequential lookup. 115 * <p>Note that ResourceBundle names are effectively classpath locations: As a 116 * consequence, the JDK's standard ResourceBundle treats dots as package separators. 117 * This means that "test.theme" is effectively equivalent to "test/theme", 118 * just like it is for programmatic {@code java.util.ResourceBundle} usage. 119 * @see #setBasename 120 * @see ResourceBundle#getBundle(String) 121 * @see ResourceBundle#getBundle(String, Locale) 122 */ 123 public void setBasenames(String... basenames) { 124 this.basenames = basenames; 125 } 126 127 /** 128 * Set the {@link ClassLoader} to load resource bundles with. 129 * Default is the thread context {@code ClassLoader}. 130 */ 131 public void setBundleClassLoader(ClassLoader classLoader) { 132 this.bundleClassLoader = classLoader; 133 } 134 135 /** 136 * Return the {@link ClassLoader} to load resource bundles with. 137 * <p>Default is the specified bundle {@code ClassLoader}, 138 * usually the thread context {@code ClassLoader}. 139 */ 140 protected ClassLoader getBundleClassLoader() { 141 return this.bundleClassLoader; 142 } 143 144 /** 145 * Set the default parent for views defined in the {@code ResourceBundle}. 146 * <p>This avoids repeated "yyy1.(parent)=xxx", "yyy2.(parent)=xxx" definitions 147 * in the bundle, especially if all defined views share the same parent. 148 * <p>The parent will typically define the view class and common attributes. 149 * Concrete views might simply consist of an URL definition then: 150 * a la "yyy1.url=/my.jsp", "yyy2.url=/your.jsp". 151 * <p>View definitions that define their own parent or carry their own 152 * class can still override this. Strictly speaking, the rule that a 153 * default parent setting does not apply to a bean definition that 154 * carries a class is there for backwards compatibility reasons. 155 * It still matches the typical use case. 156 */ 157 public void setDefaultParentView(String defaultParentView) { 158 this.defaultParentView = defaultParentView; 159 } 160 161 /** 162 * Specify Locales to initialize eagerly, rather than lazily when actually accessed. 163 * <p>Allows for pre-initialization of common Locales, eagerly checking 164 * the view configuration for those Locales. 165 */ 166 public void setLocalesToInitialize(Locale... localesToInitialize) { 167 this.localesToInitialize = localesToInitialize; 168 } 169 170 /** 171 * Specify the order value for this ViewResolver bean. 172 * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered. 173 * @see org.springframework.core.Ordered#getOrder() 174 */ 175 public void setOrder(int order) { 176 this.order = order; 177 } 178 179 @Override 180 public int getOrder() { 181 return this.order; 182 } 183 184 /** 185 * Eagerly initialize Locales if necessary. 186 * @see #setLocalesToInitialize 187 */ 188 @Override 189 public void afterPropertiesSet() throws BeansException { 190 if (this.localesToInitialize != null) { 191 for (Locale locale : this.localesToInitialize) { 192 initFactory(locale); 193 } 194 } 195 } 196 197 198 @Override 199 protected View loadView(String viewName, Locale locale) throws Exception { 200 BeanFactory factory = initFactory(locale); 201 try { 202 return factory.getBean(viewName, View.class); 203 } 204 catch (NoSuchBeanDefinitionException ex) { 205 // Allow for ViewResolver chaining... 206 return null; 207 } 208 } 209 210 /** 211 * Initialize the View {@link BeanFactory} from the {@code ResourceBundle}, 212 * for the given {@link Locale locale}. 213 * <p>Synchronized because of access by parallel threads. 214 * @param locale the target {@code Locale} 215 * @return the View factory for the given Locale 216 * @throws BeansException in case of initialization errors 217 */ 218 protected synchronized BeanFactory initFactory(Locale locale) throws BeansException { 219 // Try to find cached factory for Locale: 220 // Have we already encountered that Locale before? 221 if (isCache()) { 222 BeanFactory cachedFactory = this.localeCache.get(locale); 223 if (cachedFactory != null) { 224 return cachedFactory; 225 } 226 } 227 228 // Build list of ResourceBundle references for Locale. 229 List<ResourceBundle> bundles = new LinkedList<ResourceBundle>(); 230 for (String basename : this.basenames) { 231 ResourceBundle bundle = getBundle(basename, locale); 232 bundles.add(bundle); 233 } 234 235 // Try to find cached factory for ResourceBundle list: 236 // even if Locale was different, same bundles might have been found. 237 if (isCache()) { 238 BeanFactory cachedFactory = this.bundleCache.get(bundles); 239 if (cachedFactory != null) { 240 this.localeCache.put(locale, cachedFactory); 241 return cachedFactory; 242 } 243 } 244 245 // Create child ApplicationContext for views. 246 GenericWebApplicationContext factory = new GenericWebApplicationContext(); 247 factory.setParent(getApplicationContext()); 248 factory.setServletContext(getServletContext()); 249 250 // Load bean definitions from resource bundle. 251 PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(factory); 252 reader.setDefaultParentBean(this.defaultParentView); 253 for (ResourceBundle bundle : bundles) { 254 reader.registerBeanDefinitions(bundle); 255 } 256 257 factory.refresh(); 258 259 // Cache factory for both Locale and ResourceBundle list. 260 if (isCache()) { 261 this.localeCache.put(locale, factory); 262 this.bundleCache.put(bundles, factory); 263 } 264 265 return factory; 266 } 267 268 /** 269 * Obtain the resource bundle for the given basename and {@link Locale}. 270 * @param basename the basename to look for 271 * @param locale the {@code Locale} to look for 272 * @return the corresponding {@code ResourceBundle} 273 * @throws MissingResourceException if no matching bundle could be found 274 * @see ResourceBundle#getBundle(String, Locale, ClassLoader) 275 */ 276 protected ResourceBundle getBundle(String basename, Locale locale) throws MissingResourceException { 277 return ResourceBundle.getBundle(basename, locale, getBundleClassLoader()); 278 } 279 280 281 /** 282 * Close the bundle View factories on context shutdown. 283 */ 284 @Override 285 public void destroy() throws BeansException { 286 for (ConfigurableApplicationContext factory : this.bundleCache.values()) { 287 factory.close(); 288 } 289 this.localeCache.clear(); 290 this.bundleCache.clear(); 291 } 292 293}