View Javadoc
1   /*
2    * Copyright 2002-2013 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.LinkedHashMap;
20  import java.util.Locale;
21  import java.util.Map;
22  import java.util.concurrent.ConcurrentHashMap;
23  import javax.servlet.http.HttpServletRequest;
24  import javax.servlet.http.HttpServletResponse;
25  
26  import org.springframework.web.context.support.WebApplicationObjectSupport;
27  import org.springframework.web.servlet.View;
28  import org.springframework.web.servlet.ViewResolver;
29  
30  /**
31   * Convenient base class for {@link org.springframework.web.servlet.ViewResolver}
32   * implementations. Caches {@link org.springframework.web.servlet.View} objects
33   * once resolved: This means that view resolution won't be a performance problem,
34   * no matter how costly initial view retrieval is.
35   *
36   * <p>Subclasses need to implement the {@link #loadView} template method,
37   * building the View object for a specific view name and locale.
38   *
39   * @author Rod Johnson
40   * @author Juergen Hoeller
41   * @see #loadView
42   */
43  public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
44  
45  	/** Default maximum number of entries for the view cache: 1024 */
46  	public static final int DEFAULT_CACHE_LIMIT = 1024;
47  
48  	/** Dummy marker object for unresolved views in the cache Maps */
49  	private static final View UNRESOLVED_VIEW = new View() {
50  		@Override
51  		public String getContentType() {
52  			return null;
53  		}
54  		@Override
55  		public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
56  		}
57  	};
58  
59  
60  	/** The maximum number of entries in the cache */
61  	private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
62  
63  	/** Whether we should refrain from resolving views again if unresolved once */
64  	private boolean cacheUnresolved = true;
65  
66  	/** Fast access cache for Views, returning already cached instances without a global lock */
67  	private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);
68  
69  	/** Map from view key to View instance, synchronized for View creation */
70  	@SuppressWarnings("serial")
71  	private final Map<Object, View> viewCreationCache =
72  			new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
73  				@Override
74  				protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
75  					if (size() > getCacheLimit()) {
76  						viewAccessCache.remove(eldest.getKey());
77  						return true;
78  					}
79  					else {
80  						return false;
81  					}
82  				}
83  			};
84  
85  
86  	/**
87  	 * Specify the maximum number of entries for the view cache.
88  	 * Default is 1024.
89  	 */
90  	public void setCacheLimit(int cacheLimit) {
91  		this.cacheLimit = cacheLimit;
92  	}
93  
94  	/**
95  	 * Return the maximum number of entries for the view cache.
96  	 */
97  	public int getCacheLimit() {
98  		return this.cacheLimit;
99  	}
100 
101 	/**
102 	 * Enable or disable caching.
103 	 * <p>This is equivalent to setting the {@link #setCacheLimit "cacheLimit"}
104 	 * property to the default limit (1024) or to 0, respectively.
105 	 * <p>Default is "true": caching is enabled.
106 	 * Disable this only for debugging and development.
107 	 */
108 	public void setCache(boolean cache) {
109 		this.cacheLimit = (cache ? DEFAULT_CACHE_LIMIT : 0);
110 	}
111 
112 	/**
113 	 * Return if caching is enabled.
114 	 */
115 	public boolean isCache() {
116 		return (this.cacheLimit > 0);
117 	}
118 
119 	/**
120 	 * Whether a view name once resolved to {@code null} should be cached and
121 	 * automatically resolved to {@code null} subsequently.
122 	 * <p>Default is "true": unresolved view names are being cached, as of Spring 3.1.
123 	 * Note that this flag only applies if the general {@link #setCache "cache"}
124 	 * flag is kept at its default of "true" as well.
125 	 * <p>Of specific interest is the ability for some AbstractUrlBasedView
126 	 * implementations (FreeMarker, Velocity, Tiles) to check if an underlying
127 	 * resource exists via {@link AbstractUrlBasedView#checkResource(Locale)}.
128 	 * With this flag set to "false", an underlying resource that re-appears
129 	 * is noticed and used. With the flag set to "true", one check is made only.
130 	 */
131 	public void setCacheUnresolved(boolean cacheUnresolved) {
132 		this.cacheUnresolved = cacheUnresolved;
133 	}
134 
135 	/**
136 	 * Return if caching of unresolved views is enabled.
137 	 */
138 	public boolean isCacheUnresolved() {
139 		return this.cacheUnresolved;
140 	}
141 
142 
143 	@Override
144 	public View resolveViewName(String viewName, Locale locale) throws Exception {
145 		if (!isCache()) {
146 			return createView(viewName, locale);
147 		}
148 		else {
149 			Object cacheKey = getCacheKey(viewName, locale);
150 			View view = this.viewAccessCache.get(cacheKey);
151 			if (view == null) {
152 				synchronized (this.viewCreationCache) {
153 					view = this.viewCreationCache.get(cacheKey);
154 					if (view == null) {
155 						// Ask the subclass to create the View object.
156 						view = createView(viewName, locale);
157 						if (view == null && this.cacheUnresolved) {
158 							view = UNRESOLVED_VIEW;
159 						}
160 						if (view != null) {
161 							this.viewAccessCache.put(cacheKey, view);
162 							this.viewCreationCache.put(cacheKey, view);
163 							if (logger.isTraceEnabled()) {
164 								logger.trace("Cached view [" + cacheKey + "]");
165 							}
166 						}
167 					}
168 				}
169 			}
170 			return (view != UNRESOLVED_VIEW ? view : null);
171 		}
172 	}
173 
174 	/**
175 	 * Return the cache key for the given view name and the given locale.
176 	 * <p>Default is a String consisting of view name and locale suffix.
177 	 * Can be overridden in subclasses.
178 	 * <p>Needs to respect the locale in general, as a different locale can
179 	 * lead to a different view resource.
180 	 */
181 	protected Object getCacheKey(String viewName, Locale locale) {
182 		return viewName + "_" + locale;
183 	}
184 
185 	/**
186 	 * Provides functionality to clear the cache for a certain view.
187 	 * <p>This can be handy in case developer are able to modify views
188 	 * (e.g. Velocity templates) at runtime after which you'd need to
189 	 * clear the cache for the specified view.
190 	 * @param viewName the view name for which the cached view object
191 	 * (if any) needs to be removed
192 	 * @param locale the locale for which the view object should be removed
193 	 */
194 	public void removeFromCache(String viewName, Locale locale) {
195 		if (!isCache()) {
196 			logger.warn("View caching is SWITCHED OFF -- removal not necessary");
197 		}
198 		else {
199 			Object cacheKey = getCacheKey(viewName, locale);
200 			Object cachedView;
201 			synchronized (this.viewCreationCache) {
202 				this.viewAccessCache.remove(cacheKey);
203 				cachedView = this.viewCreationCache.remove(cacheKey);
204 			}
205 			if (logger.isDebugEnabled()) {
206 				// Some debug output might be useful...
207 				if (cachedView == null) {
208 					logger.debug("No cached instance for view '" + cacheKey + "' was found");
209 				}
210 				else {
211 					logger.debug("Cache for view " + cacheKey + " has been cleared");
212 				}
213 			}
214 		}
215 	}
216 
217 	/**
218 	 * Clear the entire view cache, removing all cached view objects.
219 	 * Subsequent resolve calls will lead to recreation of demanded view objects.
220 	 */
221 	public void clearCache() {
222 		logger.debug("Clearing entire view cache");
223 		synchronized (this.viewCreationCache) {
224 			this.viewAccessCache.clear();
225 			this.viewCreationCache.clear();
226 		}
227 	}
228 
229 
230 	/**
231 	 * Create the actual View object.
232 	 * <p>The default implementation delegates to {@link #loadView}.
233 	 * This can be overridden to resolve certain view names in a special fashion,
234 	 * before delegating to the actual {@code loadView} implementation
235 	 * provided by the subclass.
236 	 * @param viewName the name of the view to retrieve
237 	 * @param locale the Locale to retrieve the view for
238 	 * @return the View instance, or {@code null} if not found
239 	 * (optional, to allow for ViewResolver chaining)
240 	 * @throws Exception if the view couldn't be resolved
241 	 * @see #loadView
242 	 */
243 	protected View createView(String viewName, Locale locale) throws Exception {
244 		return loadView(viewName, locale);
245 	}
246 
247 	/**
248 	 * Subclasses must implement this method, building a View object
249 	 * for the specified view. The returned View objects will be
250 	 * cached by this ViewResolver base class.
251 	 * <p>Subclasses are not forced to support internationalization:
252 	 * A subclass that does not may simply ignore the locale parameter.
253 	 * @param viewName the name of the view to retrieve
254 	 * @param locale the Locale to retrieve the view for
255 	 * @return the View instance, or {@code null} if not found
256 	 * (optional, to allow for ViewResolver chaining)
257 	 * @throws Exception if the view couldn't be resolved
258 	 * @see #resolveViewName
259 	 */
260 	protected abstract View loadView(String viewName, Locale locale) throws Exception;
261 
262 }