001/*
002 * Copyright 2002-2017 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.config.annotation;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.List;
023
024import org.springframework.beans.factory.BeanFactoryUtils;
025import org.springframework.beans.factory.BeanInitializationException;
026import org.springframework.context.ApplicationContext;
027import org.springframework.core.Ordered;
028import org.springframework.util.CollectionUtils;
029import org.springframework.util.ObjectUtils;
030import org.springframework.web.accept.ContentNegotiationManager;
031import org.springframework.web.servlet.View;
032import org.springframework.web.servlet.ViewResolver;
033import org.springframework.web.servlet.view.BeanNameViewResolver;
034import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
035import org.springframework.web.servlet.view.InternalResourceViewResolver;
036import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
037import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
038import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer;
039import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
040import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
041import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
042import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
043import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
044
045/**
046 * Assist with the configuration of a chain of
047 * {@link org.springframework.web.servlet.ViewResolver ViewResolver} instances.
048 * This class is expected to be used via {@link WebMvcConfigurer#configureViewResolvers}.
049 *
050 * @author Sebastien Deleuze
051 * @author Rossen Stoyanchev
052 * @since 4.1
053 */
054public class ViewResolverRegistry {
055
056        private ContentNegotiationManager contentNegotiationManager;
057
058        private ApplicationContext applicationContext;
059
060        private ContentNegotiatingViewResolver contentNegotiatingResolver;
061
062        private final List<ViewResolver> viewResolvers = new ArrayList<ViewResolver>(4);
063
064        private Integer order;
065
066
067        /**
068         * Class constructor with {@link ContentNegotiationManager} and {@link ApplicationContext}.
069         * @since 4.3.12
070         */
071        public ViewResolverRegistry(ContentNegotiationManager contentNegotiationManager, ApplicationContext context) {
072                this.contentNegotiationManager = contentNegotiationManager;
073                this.applicationContext = context;
074        }
075
076        @Deprecated
077        public ViewResolverRegistry() {
078        }
079
080
081        /**
082         * Whether any view resolvers have been registered.
083         */
084        public boolean hasRegistrations() {
085                return (this.contentNegotiatingResolver != null || !this.viewResolvers.isEmpty());
086        }
087
088        /**
089         * Enable use of a {@link ContentNegotiatingViewResolver} to front all other
090         * configured view resolvers and select among all selected Views based on
091         * media types requested by the client (e.g. in the Accept header).
092         * <p>If invoked multiple times the provided default views will be added to
093         * any other default views that may have been configured already.
094         * @see ContentNegotiatingViewResolver#setDefaultViews
095         */
096        public void enableContentNegotiation(View... defaultViews) {
097                initContentNegotiatingViewResolver(defaultViews);
098        }
099
100        /**
101         * Enable use of a {@link ContentNegotiatingViewResolver} to front all other
102         * configured view resolvers and select among all selected Views based on
103         * media types requested by the client (e.g. in the Accept header).
104         * <p>If invoked multiple times the provided default views will be added to
105         * any other default views that may have been configured already.
106         * @see ContentNegotiatingViewResolver#setDefaultViews
107         */
108        public void enableContentNegotiation(boolean useNotAcceptableStatus, View... defaultViews) {
109                initContentNegotiatingViewResolver(defaultViews);
110                this.contentNegotiatingResolver.setUseNotAcceptableStatusCode(useNotAcceptableStatus);
111        }
112
113        private void initContentNegotiatingViewResolver(View[] defaultViews) {
114                // ContentNegotiatingResolver in the registry: elevate its precedence!
115                this.order = (this.order != null ? this.order : Ordered.HIGHEST_PRECEDENCE);
116
117                if (this.contentNegotiatingResolver != null) {
118                        if (!ObjectUtils.isEmpty(defaultViews)) {
119                                if (!CollectionUtils.isEmpty(this.contentNegotiatingResolver.getDefaultViews())) {
120                                        List<View> views = new ArrayList<View>(this.contentNegotiatingResolver.getDefaultViews());
121                                        views.addAll(Arrays.asList(defaultViews));
122                                        this.contentNegotiatingResolver.setDefaultViews(views);
123                                }
124                        }
125                }
126                else {
127                        this.contentNegotiatingResolver = new ContentNegotiatingViewResolver();
128                        this.contentNegotiatingResolver.setDefaultViews(Arrays.asList(defaultViews));
129                        this.contentNegotiatingResolver.setViewResolvers(this.viewResolvers);
130                        this.contentNegotiatingResolver.setContentNegotiationManager(this.contentNegotiationManager);
131                }
132        }
133
134        /**
135         * Register JSP view resolver using a default view name prefix of "/WEB-INF/"
136         * and a default suffix of ".jsp".
137         * <p>When this method is invoked more than once, each call will register a
138         * new ViewResolver instance. Note that since it's not easy to determine
139         * if a JSP exists without forwarding to it, using multiple JSP-based view
140         * resolvers only makes sense in combination with the "viewNames" property
141         * on the resolver indicating which view names are handled by which resolver.
142         */
143        public UrlBasedViewResolverRegistration jsp() {
144                return jsp("/WEB-INF/", ".jsp");
145        }
146
147        /**
148         * Register JSP view resolver with the specified prefix and suffix.
149         * <p>When this method is invoked more than once, each call will register a
150         * new ViewResolver instance. Note that since it's not easy to determine
151         * if a JSP exists without forwarding to it, using multiple JSP-based view
152         * resolvers only makes sense in combination with the "viewNames" property
153         * on the resolver indicating which view names are handled by which resolver.
154         */
155        public UrlBasedViewResolverRegistration jsp(String prefix, String suffix) {
156                InternalResourceViewResolver resolver = new InternalResourceViewResolver();
157                resolver.setPrefix(prefix);
158                resolver.setSuffix(suffix);
159                this.viewResolvers.add(resolver);
160                return new UrlBasedViewResolverRegistration(resolver);
161        }
162
163        /**
164         * Register Tiles 3.x view resolver.
165         * <p><strong>Note</strong> that you must also configure Tiles by adding a
166         * {@link org.springframework.web.servlet.view.tiles3.TilesConfigurer} bean.
167         */
168        public UrlBasedViewResolverRegistration tiles() {
169                if (!checkBeanOfType(TilesConfigurer.class)) {
170                        throw new BeanInitializationException("In addition to a Tiles view resolver " +
171                                        "there must also be a single TilesConfigurer bean in this web application context " +
172                                        "(or its parent).");
173                }
174                TilesRegistration registration = new TilesRegistration();
175                this.viewResolvers.add(registration.getViewResolver());
176                return registration;
177        }
178
179        /**
180         * Register a FreeMarker view resolver with an empty default view name
181         * prefix and a default suffix of ".ftl".
182         * <p><strong>Note</strong> that you must also configure FreeMarker by adding a
183         * {@link org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer} bean.
184         */
185        public UrlBasedViewResolverRegistration freeMarker() {
186                if (!checkBeanOfType(FreeMarkerConfigurer.class)) {
187                        throw new BeanInitializationException("In addition to a FreeMarker view resolver " +
188                                        "there must also be a single FreeMarkerConfig bean in this web application context " +
189                                        "(or its parent): FreeMarkerConfigurer is the usual implementation. " +
190                                        "This bean may be given any name.");
191                }
192                FreeMarkerRegistration registration = new FreeMarkerRegistration();
193                this.viewResolvers.add(registration.getViewResolver());
194                return registration;
195        }
196
197        /**
198         * Register Velocity view resolver with an empty default view name
199         * prefix and a default suffix of ".vm".
200         * <p><strong>Note</strong> that you must also configure Velocity by adding a
201         * {@link org.springframework.web.servlet.view.velocity.VelocityConfigurer} bean.
202         * @deprecated as of Spring 4.3, in favor of FreeMarker
203         */
204        @Deprecated
205        public UrlBasedViewResolverRegistration velocity() {
206                if (!checkBeanOfType(org.springframework.web.servlet.view.velocity.VelocityConfigurer.class)) {
207                        throw new BeanInitializationException("In addition to a Velocity view resolver " +
208                                        "there must also be a single VelocityConfig bean in this web application context " +
209                                        "(or its parent): VelocityConfigurer is the usual implementation. " +
210                                        "This bean may be given any name.");
211                }
212                VelocityRegistration registration = new VelocityRegistration();
213                this.viewResolvers.add(registration.getViewResolver());
214                return registration;
215        }
216
217        /**
218         * Register a Groovy markup view resolver with an empty default view name
219         * prefix and a default suffix of ".tpl".
220         */
221        public UrlBasedViewResolverRegistration groovy() {
222                if (!checkBeanOfType(GroovyMarkupConfigurer.class)) {
223                        throw new BeanInitializationException("In addition to a Groovy markup view resolver " +
224                                        "there must also be a single GroovyMarkupConfig bean in this web application context " +
225                                        "(or its parent): GroovyMarkupConfigurer is the usual implementation. " +
226                                        "This bean may be given any name.");
227                }
228                GroovyMarkupRegistration registration = new GroovyMarkupRegistration();
229                this.viewResolvers.add(registration.getViewResolver());
230                return registration;
231        }
232
233        /**
234         * Register a script template view resolver with an empty default view name prefix and suffix.
235         * @since 4.2
236         */
237        public UrlBasedViewResolverRegistration scriptTemplate() {
238                if (!checkBeanOfType(ScriptTemplateConfigurer.class)) {
239                        throw new BeanInitializationException("In addition to a script template view resolver " +
240                                        "there must also be a single ScriptTemplateConfig bean in this web application context " +
241                                        "(or its parent): ScriptTemplateConfigurer is the usual implementation. " +
242                                        "This bean may be given any name.");
243                }
244                ScriptRegistration registration = new ScriptRegistration();
245                this.viewResolvers.add(registration.getViewResolver());
246                return registration;
247        }
248
249        /**
250         * Register a bean name view resolver that interprets view names as the names
251         * of {@link org.springframework.web.servlet.View} beans.
252         */
253        public void beanName() {
254                BeanNameViewResolver resolver = new BeanNameViewResolver();
255                this.viewResolvers.add(resolver);
256        }
257
258        /**
259         * Register a {@link ViewResolver} bean instance. This may be useful to
260         * configure a custom (or 3rd party) resolver implementation. It may also be
261         * used as an alternative to other registration methods in this class when
262         * they don't expose some more advanced property that needs to be set.
263         */
264        public void viewResolver(ViewResolver viewResolver) {
265                if (viewResolver instanceof ContentNegotiatingViewResolver) {
266                        throw new BeanInitializationException(
267                                        "addViewResolver cannot be used to configure a ContentNegotiatingViewResolver. " +
268                                        "Please use the method enableContentNegotiation instead.");
269                }
270                this.viewResolvers.add(viewResolver);
271        }
272
273        /**
274         * ViewResolver's registered through this registry are encapsulated in an
275         * instance of {@link org.springframework.web.servlet.view.ViewResolverComposite
276         * ViewResolverComposite} and follow the order of registration.
277         * This property determines the order of the ViewResolverComposite itself
278         * relative to any additional ViewResolver's (not registered here) present in
279         * the Spring configuration
280         * <p>By default this property is not set, which means the resolver is ordered
281         * at {@link Ordered#LOWEST_PRECEDENCE} unless content negotiation is enabled
282         * in which case the order (if not set explicitly) is changed to
283         * {@link Ordered#HIGHEST_PRECEDENCE}.
284         */
285        public void order(int order) {
286                this.order = order;
287        }
288
289
290        protected int getOrder() {
291                return (this.order != null ? this.order : Ordered.LOWEST_PRECEDENCE);
292        }
293
294        protected List<ViewResolver> getViewResolvers() {
295                if (this.contentNegotiatingResolver != null) {
296                        return Collections.<ViewResolver>singletonList(this.contentNegotiatingResolver);
297                }
298                else {
299                        return this.viewResolvers;
300                }
301        }
302
303        private boolean checkBeanOfType(Class<?> beanType) {
304                return (this.applicationContext == null ||
305                                !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
306                                                this.applicationContext, beanType, false, false)));
307        }
308
309        @Deprecated
310        protected boolean hasBeanOfType(Class<?> beanType) {
311                return !ObjectUtils.isEmpty(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
312                                this.applicationContext, beanType, false, false));
313        }
314
315        @Deprecated
316        protected void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
317                this.contentNegotiationManager = contentNegotiationManager;
318        }
319
320        @Deprecated
321        protected void setApplicationContext(ApplicationContext applicationContext) {
322                this.applicationContext = applicationContext;
323        }
324
325
326        private static class TilesRegistration extends UrlBasedViewResolverRegistration {
327
328                public TilesRegistration() {
329                        super(new TilesViewResolver());
330                }
331        }
332
333
334        private static class VelocityRegistration extends UrlBasedViewResolverRegistration {
335
336                @SuppressWarnings("deprecation")
337                public VelocityRegistration() {
338                        super(new org.springframework.web.servlet.view.velocity.VelocityViewResolver());
339                        getViewResolver().setSuffix(".vm");
340                }
341        }
342
343
344        private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration {
345
346                public FreeMarkerRegistration() {
347                        super(new FreeMarkerViewResolver());
348                        getViewResolver().setSuffix(".ftl");
349                }
350        }
351
352
353        private static class GroovyMarkupRegistration extends UrlBasedViewResolverRegistration {
354
355                public GroovyMarkupRegistration() {
356                        super(new GroovyMarkupViewResolver());
357                        getViewResolver().setSuffix(".tpl");
358                }
359        }
360
361
362        private static class ScriptRegistration extends UrlBasedViewResolverRegistration {
363
364                public ScriptRegistration() {
365                        super(new ScriptTemplateViewResolver());
366                        getViewResolver();
367                }
368        }
369
370}