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