001/*
002 * Copyright 2002-2013 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.LinkedHashMap;
020import java.util.Locale;
021import java.util.Map;
022import java.util.concurrent.ConcurrentHashMap;
023import javax.servlet.http.HttpServletRequest;
024import javax.servlet.http.HttpServletResponse;
025
026import org.springframework.web.context.support.WebApplicationObjectSupport;
027import org.springframework.web.servlet.View;
028import org.springframework.web.servlet.ViewResolver;
029
030/**
031 * Convenient base class for {@link org.springframework.web.servlet.ViewResolver}
032 * implementations. Caches {@link org.springframework.web.servlet.View} objects
033 * once resolved: This means that view resolution won't be a performance problem,
034 * no matter how costly initial view retrieval is.
035 *
036 * <p>Subclasses need to implement the {@link #loadView} template method,
037 * building the View object for a specific view name and locale.
038 *
039 * @author Rod Johnson
040 * @author Juergen Hoeller
041 * @see #loadView
042 */
043public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
044
045        /** Default maximum number of entries for the view cache: 1024 */
046        public static final int DEFAULT_CACHE_LIMIT = 1024;
047
048        /** Dummy marker object for unresolved views in the cache Maps */
049        private static final View UNRESOLVED_VIEW = new View() {
050                @Override
051                public String getContentType() {
052                        return null;
053                }
054                @Override
055                public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
056                }
057        };
058
059
060        /** The maximum number of entries in the cache */
061        private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
062
063        /** Whether we should refrain from resolving views again if unresolved once */
064        private boolean cacheUnresolved = true;
065
066        /** Fast access cache for Views, returning already cached instances without a global lock */
067        private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);
068
069        /** Map from view key to View instance, synchronized for View creation */
070        @SuppressWarnings("serial")
071        private final Map<Object, View> viewCreationCache =
072                        new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
073                                @Override
074                                protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
075                                        if (size() > getCacheLimit()) {
076                                                viewAccessCache.remove(eldest.getKey());
077                                                return true;
078                                        }
079                                        else {
080                                                return false;
081                                        }
082                                }
083                        };
084
085
086        /**
087         * Specify the maximum number of entries for the view cache.
088         * Default is 1024.
089         */
090        public void setCacheLimit(int cacheLimit) {
091                this.cacheLimit = cacheLimit;
092        }
093
094        /**
095         * Return the maximum number of entries for the view cache.
096         */
097        public int getCacheLimit() {
098                return this.cacheLimit;
099        }
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}