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