001/*
002 * Copyright 2002-2019 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;
023
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026
027import org.springframework.lang.Nullable;
028import org.springframework.util.Assert;
029import org.springframework.web.context.support.WebApplicationObjectSupport;
030import org.springframework.web.servlet.View;
031import org.springframework.web.servlet.ViewResolver;
032
033/**
034 * Convenient base class for {@link org.springframework.web.servlet.ViewResolver}
035 * implementations. Caches {@link org.springframework.web.servlet.View} objects
036 * once resolved: This means that view resolution won't be a performance problem,
037 * no matter how costly initial view retrieval is.
038 *
039 * <p>Subclasses need to implement the {@link #loadView} template method,
040 * building the View object for a specific view name and locale.
041 *
042 * @author Rod Johnson
043 * @author Juergen Hoeller
044 * @see #loadView
045 */
046public abstract class AbstractCachingViewResolver extends WebApplicationObjectSupport implements ViewResolver {
047
048        /** Default maximum number of entries for the view cache: 1024. */
049        public static final int DEFAULT_CACHE_LIMIT = 1024;
050
051        /** Dummy marker object for unresolved views in the cache Maps. */
052        private static final View UNRESOLVED_VIEW = new View() {
053                @Override
054                @Nullable
055                public String getContentType() {
056                        return null;
057                }
058                @Override
059                public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
060                }
061        };
062
063        /** Default cache filter that always caches. */
064        private static final CacheFilter DEFAULT_CACHE_FILTER = (view, viewName, locale) -> true;
065
066
067        /** The maximum number of entries in the cache. */
068        private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
069
070        /** Whether we should refrain from resolving views again if unresolved once. */
071        private boolean cacheUnresolved = true;
072
073        /** Filter function that determines if view should be cached. */
074        private CacheFilter cacheFilter = DEFAULT_CACHE_FILTER;
075
076        /** Fast access cache for Views, returning already cached instances without a global lock. */
077        private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT);
078
079        /** Map from view key to View instance, synchronized for View creation. */
080        @SuppressWarnings("serial")
081        private final Map<Object, View> viewCreationCache =
082                        new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
083                                @Override
084                                protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
085                                        if (size() > getCacheLimit()) {
086                                                viewAccessCache.remove(eldest.getKey());
087                                                return true;
088                                        }
089                                        else {
090                                                return false;
091                                        }
092                                }
093                        };
094
095
096        /**
097         * Specify the maximum number of entries for the view cache.
098         * Default is 1024.
099         */
100        public void setCacheLimit(int cacheLimit) {
101                this.cacheLimit = cacheLimit;
102        }
103
104        /**
105         * Return the maximum number of entries for the view cache.
106         */
107        public int getCacheLimit() {
108                return this.cacheLimit;
109        }
110
111        /**
112         * Enable or disable caching.
113         * <p>This is equivalent to setting the {@link #setCacheLimit "cacheLimit"}
114         * property to the default limit (1024) or to 0, respectively.
115         * <p>Default is "true": caching is enabled.
116         * Disable this only for debugging and development.
117         */
118        public void setCache(boolean cache) {
119                this.cacheLimit = (cache ? DEFAULT_CACHE_LIMIT : 0);
120        }
121
122        /**
123         * Return if caching is enabled.
124         */
125        public boolean isCache() {
126                return (this.cacheLimit > 0);
127        }
128
129        /**
130         * Whether a view name once resolved to {@code null} should be cached and
131         * automatically resolved to {@code null} subsequently.
132         * <p>Default is "true": unresolved view names are being cached, as of Spring 3.1.
133         * Note that this flag only applies if the general {@link #setCache "cache"}
134         * flag is kept at its default of "true" as well.
135         * <p>Of specific interest is the ability for some AbstractUrlBasedView
136         * implementations (FreeMarker, Tiles) to check if an underlying resource
137         * exists via {@link AbstractUrlBasedView#checkResource(Locale)}.
138         * With this flag set to "false", an underlying resource that re-appears
139         * is noticed and used. With the flag set to "true", one check is made only.
140         */
141        public void setCacheUnresolved(boolean cacheUnresolved) {
142                this.cacheUnresolved = cacheUnresolved;
143        }
144
145        /**
146         * Return if caching of unresolved views is enabled.
147         */
148        public boolean isCacheUnresolved() {
149                return this.cacheUnresolved;
150        }
151
152        /**
153         * Sets the filter that determines if view should be cached.
154         * Default behaviour is to cache all views.
155         * @since 5.2
156         */
157        public void setCacheFilter(CacheFilter cacheFilter) {
158                Assert.notNull(cacheFilter, "CacheFilter must not be null");
159                this.cacheFilter = cacheFilter;
160        }
161
162        /**
163         * Return filter function that determines if view should be cached.
164         * @since 5.2
165         */
166        public CacheFilter getCacheFilter() {
167                return this.cacheFilter;
168        }
169
170        @Override
171        @Nullable
172        public View resolveViewName(String viewName, Locale locale) throws Exception {
173                if (!isCache()) {
174                        return createView(viewName, locale);
175                }
176                else {
177                        Object cacheKey = getCacheKey(viewName, locale);
178                        View view = this.viewAccessCache.get(cacheKey);
179                        if (view == null) {
180                                synchronized (this.viewCreationCache) {
181                                        view = this.viewCreationCache.get(cacheKey);
182                                        if (view == null) {
183                                                // Ask the subclass to create the View object.
184                                                view = createView(viewName, locale);
185                                                if (view == null && this.cacheUnresolved) {
186                                                        view = UNRESOLVED_VIEW;
187                                                }
188                                                if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
189                                                        this.viewAccessCache.put(cacheKey, view);
190                                                        this.viewCreationCache.put(cacheKey, view);
191                                                }
192                                        }
193                                }
194                        }
195                        else {
196                                if (logger.isTraceEnabled()) {
197                                        logger.trace(formatKey(cacheKey) + "served from cache");
198                                }
199                        }
200                        return (view != UNRESOLVED_VIEW ? view : null);
201                }
202        }
203
204        private static String formatKey(Object cacheKey) {
205                return "View with key [" + cacheKey + "] ";
206        }
207
208        /**
209         * Return the cache key for the given view name and the given locale.
210         * <p>Default is a String consisting of view name and locale suffix.
211         * Can be overridden in subclasses.
212         * <p>Needs to respect the locale in general, as a different locale can
213         * lead to a different view resource.
214         */
215        protected Object getCacheKey(String viewName, Locale locale) {
216                return viewName + '_' + locale;
217        }
218
219        /**
220         * Provides functionality to clear the cache for a certain view.
221         * <p>This can be handy in case developer are able to modify views
222         * (e.g. FreeMarker templates) at runtime after which you'd need to
223         * clear the cache for the specified view.
224         * @param viewName the view name for which the cached view object
225         * (if any) needs to be removed
226         * @param locale the locale for which the view object should be removed
227         */
228        public void removeFromCache(String viewName, Locale locale) {
229                if (!isCache()) {
230                        logger.warn("Caching is OFF (removal not necessary)");
231                }
232                else {
233                        Object cacheKey = getCacheKey(viewName, locale);
234                        Object cachedView;
235                        synchronized (this.viewCreationCache) {
236                                this.viewAccessCache.remove(cacheKey);
237                                cachedView = this.viewCreationCache.remove(cacheKey);
238                        }
239                        if (logger.isDebugEnabled()) {
240                                // Some debug output might be useful...
241                                logger.debug(formatKey(cacheKey) +
242                                                (cachedView != null ? "cleared from cache" : "not found in the cache"));
243                        }
244                }
245        }
246
247        /**
248         * Clear the entire view cache, removing all cached view objects.
249         * Subsequent resolve calls will lead to recreation of demanded view objects.
250         */
251        public void clearCache() {
252                logger.debug("Clearing all views from the cache");
253                synchronized (this.viewCreationCache) {
254                        this.viewAccessCache.clear();
255                        this.viewCreationCache.clear();
256                }
257        }
258
259
260        /**
261         * Create the actual View object.
262         * <p>The default implementation delegates to {@link #loadView}.
263         * This can be overridden to resolve certain view names in a special fashion,
264         * before delegating to the actual {@code loadView} implementation
265         * provided by the subclass.
266         * @param viewName the name of the view to retrieve
267         * @param locale the Locale to retrieve the view for
268         * @return the View instance, or {@code null} if not found
269         * (optional, to allow for ViewResolver chaining)
270         * @throws Exception if the view couldn't be resolved
271         * @see #loadView
272         */
273        @Nullable
274        protected View createView(String viewName, Locale locale) throws Exception {
275                return loadView(viewName, locale);
276        }
277
278        /**
279         * Subclasses must implement this method, building a View object
280         * for the specified view. The returned View objects will be
281         * cached by this ViewResolver base class.
282         * <p>Subclasses are not forced to support internationalization:
283         * A subclass that does not may simply ignore the locale parameter.
284         * @param viewName the name of the view to retrieve
285         * @param locale the Locale to retrieve the view for
286         * @return the View instance, or {@code null} if not found
287         * (optional, to allow for ViewResolver chaining)
288         * @throws Exception if the view couldn't be resolved
289         * @see #resolveViewName
290         */
291        @Nullable
292        protected abstract View loadView(String viewName, Locale locale) throws Exception;
293
294
295        /**
296         * Filter that determines if view should be cached.
297         *
298         * @author Sergey Galkin
299         * @author Arjen Poutsma
300         * @since 5.2
301         */
302        @FunctionalInterface
303        public interface CacheFilter {
304
305                /**
306                 * Indicates whether the given view should be cached.
307                 * The name and locale used to resolve the view are also provided.
308                 * @param view the view
309                 * @param viewName the name used to resolve the {@code view}
310                 * @param locale the locale used to resolve the {@code view}
311                 * @return {@code true} if the view should be cached; {@code false} otherwise
312                 */
313                boolean filter(View view, String viewName, Locale locale);
314        }
315
316}