View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.view;
18  
19  import java.util.HashMap;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.Map;
24  import java.util.MissingResourceException;
25  import java.util.ResourceBundle;
26  
27  import org.springframework.beans.BeansException;
28  import org.springframework.beans.factory.BeanFactory;
29  import org.springframework.beans.factory.DisposableBean;
30  import org.springframework.beans.factory.InitializingBean;
31  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
32  import org.springframework.beans.factory.support.PropertiesBeanDefinitionReader;
33  import org.springframework.context.ConfigurableApplicationContext;
34  import org.springframework.core.Ordered;
35  import org.springframework.web.context.support.GenericWebApplicationContext;
36  import org.springframework.web.servlet.View;
37  
38  /**
39   * A {@link org.springframework.web.servlet.ViewResolver} implementation that uses
40   * bean definitions in a {@link ResourceBundle}, specified by the bundle basename.
41   *
42   * <p>The bundle is typically defined in a properties file, located in the classpath.
43   * The default bundle basename is "views".
44   *
45   * <p>This {@code ViewResolver} supports localized view definitions, using the
46   * default support of {@link java.util.PropertyResourceBundle}. For example, the
47   * basename "views" will be resolved as class path resources "views_de_AT.properties",
48   * "views_de.properties", "views.properties" - for a given Locale "de_AT".
49   *
50   * <p>Note: This {@code ViewResolver} implements the {@link Ordered} interface
51   * in order to allow for flexible participation in {@code ViewResolver} chaining.
52   * For example, some special views could be defined via this {@code ViewResolver}
53   * (giving it 0 as "order" value), while all remaining views could be resolved by
54   * a {@link UrlBasedViewResolver}.
55   *
56   * @author Rod Johnson
57   * @author Juergen Hoeller
58   * @see java.util.ResourceBundle#getBundle
59   * @see java.util.PropertyResourceBundle
60   * @see UrlBasedViewResolver
61   */
62  public class ResourceBundleViewResolver extends AbstractCachingViewResolver
63  		implements Ordered, InitializingBean, DisposableBean {
64  
65  	/** The default basename if no other basename is supplied. */
66  	public final static String DEFAULT_BASENAME = "views";
67  
68  
69  	private int order = Integer.MAX_VALUE;  // default: same as non-Ordered
70  
71  	private String[] basenames = new String[] {DEFAULT_BASENAME};
72  
73  	private ClassLoader bundleClassLoader = Thread.currentThread().getContextClassLoader();
74  
75  	private String defaultParentView;
76  
77  	private Locale[] localesToInitialize;
78  
79  	/* Locale -> BeanFactory */
80  	private final Map<Locale, BeanFactory> localeCache =
81  			new HashMap<Locale, BeanFactory>();
82  
83  	/* List of ResourceBundle -> BeanFactory */
84  	private final Map<List<ResourceBundle>, ConfigurableApplicationContext> bundleCache =
85  			new HashMap<List<ResourceBundle>, ConfigurableApplicationContext>();
86  
87  
88  	public void setOrder(int order) {
89  		this.order = order;
90  	}
91  
92  	@Override
93  	public int getOrder() {
94  		return this.order;
95  	}
96  
97  	/**
98  	 * Set a single basename, following {@link java.util.ResourceBundle} conventions.
99  	 * The default is "views".
100 	 * <p>{@code ResourceBundle} supports different suffixes. For example,
101 	 * a base name of "views" might map to {@code ResourceBundle} files
102 	 * "views", "views_en_au" and "views_de".
103 	 * <p>Note that ResourceBundle names are effectively classpath locations: As a
104 	 * consequence, the JDK's standard ResourceBundle treats dots as package separators.
105 	 * This means that "test.theme" is effectively equivalent to "test/theme",
106 	 * just like it is for programmatic {@code java.util.ResourceBundle} usage.
107 	 * @see #setBasenames
108 	 * @see java.util.ResourceBundle#getBundle(String)
109 	 */
110 	public void setBasename(String basename) {
111 		setBasenames(basename);
112 	}
113 
114 	/**
115 	 * Set an array of basenames, each following {@link java.util.ResourceBundle}
116 	 * conventions. The default is a single basename "views".
117 	 * <p>{@code ResourceBundle} supports different suffixes. For example,
118 	 * a base name of "views" might map to {@code ResourceBundle} files
119 	 * "views", "views_en_au" and "views_de".
120 	 * <p>The associated resource bundles will be checked sequentially
121 	 * when resolving a message code. Note that message definitions in a
122 	 * <i>previous</i> resource bundle will override ones in a later bundle,
123 	 * due to the sequential lookup.
124 	 * <p>Note that ResourceBundle names are effectively classpath locations: As a
125 	 * consequence, the JDK's standard ResourceBundle treats dots as package separators.
126 	 * This means that "test.theme" is effectively equivalent to "test/theme",
127 	 * just like it is for programmatic {@code java.util.ResourceBundle} usage.
128 	 * @see #setBasename
129 	 * @see java.util.ResourceBundle#getBundle(String)
130 	 */
131 	public void setBasenames(String... basenames) {
132 		this.basenames = basenames;
133 	}
134 
135 	/**
136 	 * Set the {@link ClassLoader} to load resource bundles with.
137 	 * Default is the thread context {@code ClassLoader}.
138 	 */
139 	public void setBundleClassLoader(ClassLoader classLoader) {
140 		this.bundleClassLoader = classLoader;
141 	}
142 
143 	/**
144 	 * Return the {@link ClassLoader} to load resource bundles with.
145 	 * <p>Default is the specified bundle {@code ClassLoader},
146 	 * usually the thread context {@code ClassLoader}.
147 	 */
148 	protected ClassLoader getBundleClassLoader() {
149 		return this.bundleClassLoader;
150 	}
151 
152 	/**
153 	 * Set the default parent for views defined in the {@code ResourceBundle}.
154 	 * <p>This avoids repeated "yyy1.(parent)=xxx", "yyy2.(parent)=xxx" definitions
155 	 * in the bundle, especially if all defined views share the same parent.
156 	 * <p>The parent will typically define the view class and common attributes.
157 	 * Concrete views might simply consist of an URL definition then:
158 	 * a la "yyy1.url=/my.jsp", "yyy2.url=/your.jsp".
159 	 * <p>View definitions that define their own parent or carry their own
160 	 * class can still override this. Strictly speaking, the rule that a
161 	 * default parent setting does not apply to a bean definition that
162 	 * carries a class is there for backwards compatibility reasons.
163 	 * It still matches the typical use case.
164 	 */
165 	public void setDefaultParentView(String defaultParentView) {
166 		this.defaultParentView = defaultParentView;
167 	}
168 
169 	/**
170 	 * Specify Locales to initialize eagerly, rather than lazily when actually accessed.
171 	 * <p>Allows for pre-initialization of common Locales, eagerly checking
172 	 * the view configuration for those Locales.
173 	 */
174 	public void setLocalesToInitialize(Locale... localesToInitialize) {
175 		this.localesToInitialize = localesToInitialize;
176 	}
177 
178 	/**
179 	 * Eagerly initialize Locales if necessary.
180 	 * @see #setLocalesToInitialize
181 	 */
182 	@Override
183 	public void afterPropertiesSet() throws BeansException {
184 		if (this.localesToInitialize != null) {
185 			for (Locale locale : this.localesToInitialize) {
186 				initFactory(locale);
187 			}
188 		}
189 	}
190 
191 
192 	@Override
193 	protected View loadView(String viewName, Locale locale) throws Exception {
194 		BeanFactory factory = initFactory(locale);
195 		try {
196 			return factory.getBean(viewName, View.class);
197 		}
198 		catch (NoSuchBeanDefinitionException ex) {
199 			// Allow for ViewResolver chaining...
200 			return null;
201 		}
202 	}
203 
204 	/**
205 	 * Initialize the View {@link BeanFactory} from the {@code ResourceBundle},
206 	 * for the given {@link Locale locale}.
207 	 * <p>Synchronized because of access by parallel threads.
208 	 * @param locale the target {@code Locale}
209 	 * @return the View factory for the given Locale
210 	 * @throws BeansException in case of initialization errors
211 	 */
212 	protected synchronized BeanFactory initFactory(Locale locale) throws BeansException {
213 		// Try to find cached factory for Locale:
214 		// Have we already encountered that Locale before?
215 		if (isCache()) {
216 			BeanFactory cachedFactory = this.localeCache.get(locale);
217 			if (cachedFactory != null) {
218 				return cachedFactory;
219 			}
220 		}
221 
222 		// Build list of ResourceBundle references for Locale.
223 		List<ResourceBundle> bundles = new LinkedList<ResourceBundle>();
224 		for (String basename : this.basenames) {
225 			ResourceBundle bundle = getBundle(basename, locale);
226 			bundles.add(bundle);
227 		}
228 
229 		// Try to find cached factory for ResourceBundle list:
230 		// even if Locale was different, same bundles might have been found.
231 		if (isCache()) {
232 			BeanFactory cachedFactory = this.bundleCache.get(bundles);
233 			if (cachedFactory != null) {
234 				this.localeCache.put(locale, cachedFactory);
235 				return cachedFactory;
236 			}
237 		}
238 
239 		// Create child ApplicationContext for views.
240 		GenericWebApplicationContext factory = new GenericWebApplicationContext();
241 		factory.setParent(getApplicationContext());
242 		factory.setServletContext(getServletContext());
243 
244 		// Load bean definitions from resource bundle.
245 		PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(factory);
246 		reader.setDefaultParentBean(this.defaultParentView);
247 		for (ResourceBundle bundle : bundles) {
248 			reader.registerBeanDefinitions(bundle);
249 		}
250 
251 		factory.refresh();
252 
253 		// Cache factory for both Locale and ResourceBundle list.
254 		if (isCache()) {
255 			this.localeCache.put(locale, factory);
256 			this.bundleCache.put(bundles, factory);
257 		}
258 
259 		return factory;
260 	}
261 
262 	/**
263 	 * Obtain the resource bundle for the given basename and {@link Locale}.
264 	 * @param basename the basename to look for
265 	 * @param locale the {@code Locale} to look for
266 	 * @return the corresponding {@code ResourceBundle}
267 	 * @throws MissingResourceException if no matching bundle could be found
268 	 * @see java.util.ResourceBundle#getBundle(String, java.util.Locale, ClassLoader)
269 	 */
270 	protected ResourceBundle getBundle(String basename, Locale locale) throws MissingResourceException {
271 		return ResourceBundle.getBundle(basename, locale, getBundleClassLoader());
272 	}
273 
274 
275 	/**
276 	 * Close the bundle View factories on context shutdown.
277 	 */
278 	@Override
279 	public void destroy() throws BeansException {
280 		for (ConfigurableApplicationContext factory : this.bundleCache.values()) {
281 			factory.close();
282 		}
283 		this.localeCache.clear();
284 		this.bundleCache.clear();
285 	}
286 
287 }