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.HashMap;
020import java.util.Locale;
021import java.util.Map;
022import java.util.Properties;
023
024import javax.servlet.http.HttpServletResponse;
025
026import org.springframework.beans.BeanUtils;
027import org.springframework.context.ApplicationContext;
028import org.springframework.core.Ordered;
029import org.springframework.lang.Nullable;
030import org.springframework.util.Assert;
031import org.springframework.util.CollectionUtils;
032import org.springframework.util.PatternMatchUtils;
033import org.springframework.web.servlet.View;
034
035/**
036 * Simple implementation of the {@link org.springframework.web.servlet.ViewResolver}
037 * interface, allowing for direct resolution of symbolic view names to URLs,
038 * without explicit mapping definitions. This is useful if your symbolic names
039 * match the names of your view resources in a straightforward manner
040 * (i.e. the symbolic name is the unique part of the resource's filename),
041 * without the need for a dedicated mapping to be defined for each view.
042 *
043 * <p>Supports {@link AbstractUrlBasedView} subclasses like {@link InternalResourceView}
044 * and {@link org.springframework.web.servlet.view.freemarker.FreeMarkerView}.
045 * The view class for all views generated by this resolver can be specified
046 * via the "viewClass" property.
047 *
048 * <p>View names can either be resource URLs themselves, or get augmented by a
049 * specified prefix and/or suffix. Exporting an attribute that holds the
050 * RequestContext to all views is explicitly supported.
051 *
052 * <p>Example: prefix="/WEB-INF/jsp/", suffix=".jsp", viewname="test" ->
053 * "/WEB-INF/jsp/test.jsp"
054 *
055 * <p>As a special feature, redirect URLs can be specified via the "redirect:"
056 * prefix. E.g.: "redirect:myAction" will trigger a redirect to the given
057 * URL, rather than resolution as standard view name. This is typically used
058 * for redirecting to a controller URL after finishing a form workflow.
059 *
060 * <p>Furthermore, forward URLs can be specified via the "forward:" prefix.
061 * E.g.: "forward:myAction" will trigger a forward to the given URL, rather than
062 * resolution as standard view name. This is typically used for controller URLs;
063 * it is not supposed to be used for JSP URLs - use logical view names there.
064 *
065 * <p>Note: This class does not support localized resolution, i.e. resolving
066 * a symbolic view name to different resources depending on the current locale.
067 *
068 * <p><b>Note:</b> When chaining ViewResolvers, a UrlBasedViewResolver will check whether
069 * the {@linkplain AbstractUrlBasedView#checkResource specified resource actually exists}.
070 * However, with {@link InternalResourceView}, it is not generally possible to
071 * determine the existence of the target resource upfront. In such a scenario,
072 * a UrlBasedViewResolver will always return a View for any given view name;
073 * as a consequence, it should be configured as the last ViewResolver in the chain.
074 *
075 * @author Juergen Hoeller
076 * @author Rob Harrop
077 * @author Sam Brannen
078 * @since 13.12.2003
079 * @see #setViewClass
080 * @see #setPrefix
081 * @see #setSuffix
082 * @see #setRequestContextAttribute
083 * @see #REDIRECT_URL_PREFIX
084 * @see AbstractUrlBasedView
085 * @see InternalResourceView
086 * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView
087 */
088public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered {
089
090        /**
091         * Prefix for special view names that specify a redirect URL (usually
092         * to a controller after a form has been submitted and processed).
093         * Such view names will not be resolved in the configured default
094         * way but rather be treated as special shortcut.
095         */
096        public static final String REDIRECT_URL_PREFIX = "redirect:";
097
098        /**
099         * Prefix for special view names that specify a forward URL (usually
100         * to a controller after a form has been submitted and processed).
101         * Such view names will not be resolved in the configured default
102         * way but rather be treated as special shortcut.
103         */
104        public static final String FORWARD_URL_PREFIX = "forward:";
105
106
107        @Nullable
108        private Class<?> viewClass;
109
110        private String prefix = "";
111
112        private String suffix = "";
113
114        @Nullable
115        private String contentType;
116
117        private boolean redirectContextRelative = true;
118
119        private boolean redirectHttp10Compatible = true;
120
121        @Nullable
122        private String[] redirectHosts;
123
124        @Nullable
125        private String requestContextAttribute;
126
127        /** Map of static attributes, keyed by attribute name (String). */
128        private final Map<String, Object> staticAttributes = new HashMap<>();
129
130        @Nullable
131        private Boolean exposePathVariables;
132
133        @Nullable
134        private Boolean exposeContextBeansAsAttributes;
135
136        @Nullable
137        private String[] exposedContextBeanNames;
138
139        @Nullable
140        private String[] viewNames;
141
142        private int order = Ordered.LOWEST_PRECEDENCE;
143
144
145        /**
146         * Set the view class that should be used to create views.
147         * @param viewClass class that is assignable to the required view class
148         * (by default, AbstractUrlBasedView)
149         * @see AbstractUrlBasedView
150         */
151        public void setViewClass(@Nullable Class<?> viewClass) {
152                if (viewClass != null && !requiredViewClass().isAssignableFrom(viewClass)) {
153                        throw new IllegalArgumentException("Given view class [" + viewClass.getName() +
154                                        "] is not of type [" + requiredViewClass().getName() + "]");
155                }
156                this.viewClass = viewClass;
157        }
158
159        /**
160         * Return the view class to be used to create views.
161         */
162        @Nullable
163        protected Class<?> getViewClass() {
164                return this.viewClass;
165        }
166
167        /**
168         * Return the required type of view for this resolver.
169         * This implementation returns AbstractUrlBasedView.
170         * @see AbstractUrlBasedView
171         */
172        protected Class<?> requiredViewClass() {
173                return AbstractUrlBasedView.class;
174        }
175
176        /**
177         * Set the prefix that gets prepended to view names when building a URL.
178         */
179        public void setPrefix(@Nullable String prefix) {
180                this.prefix = (prefix != null ? prefix : "");
181        }
182
183        /**
184         * Return the prefix that gets prepended to view names when building a URL.
185         */
186        protected String getPrefix() {
187                return this.prefix;
188        }
189
190        /**
191         * Set the suffix that gets appended to view names when building a URL.
192         */
193        public void setSuffix(@Nullable String suffix) {
194                this.suffix = (suffix != null ? suffix : "");
195        }
196
197        /**
198         * Return the suffix that gets appended to view names when building a URL.
199         */
200        protected String getSuffix() {
201                return this.suffix;
202        }
203
204        /**
205         * Set the content type for all views.
206         * <p>May be ignored by view classes if the view itself is assumed
207         * to set the content type, e.g. in case of JSPs.
208         */
209        public void setContentType(@Nullable String contentType) {
210                this.contentType = contentType;
211        }
212
213        /**
214         * Return the content type for all views, if any.
215         */
216        @Nullable
217        protected String getContentType() {
218                return this.contentType;
219        }
220
221        /**
222         * Set whether to interpret a given redirect URL that starts with a
223         * slash ("/") as relative to the current ServletContext, i.e. as
224         * relative to the web application root.
225         * <p>Default is "true": A redirect URL that starts with a slash will be
226         * interpreted as relative to the web application root, i.e. the context
227         * path will be prepended to the URL.
228         * <p><b>Redirect URLs can be specified via the "redirect:" prefix.</b>
229         * E.g.: "redirect:myAction"
230         * @see RedirectView#setContextRelative
231         * @see #REDIRECT_URL_PREFIX
232         */
233        public void setRedirectContextRelative(boolean redirectContextRelative) {
234                this.redirectContextRelative = redirectContextRelative;
235        }
236
237        /**
238         * Return whether to interpret a given redirect URL that starts with a
239         * slash ("/") as relative to the current ServletContext, i.e. as
240         * relative to the web application root.
241         */
242        protected boolean isRedirectContextRelative() {
243                return this.redirectContextRelative;
244        }
245
246        /**
247         * Set whether redirects should stay compatible with HTTP 1.0 clients.
248         * <p>In the default implementation, this will enforce HTTP status code 302
249         * in any case, i.e. delegate to {@code HttpServletResponse.sendRedirect}.
250         * Turning this off will send HTTP status code 303, which is the correct
251         * code for HTTP 1.1 clients, but not understood by HTTP 1.0 clients.
252         * <p>Many HTTP 1.1 clients treat 302 just like 303, not making any
253         * difference. However, some clients depend on 303 when redirecting
254         * after a POST request; turn this flag off in such a scenario.
255         * <p><b>Redirect URLs can be specified via the "redirect:" prefix.</b>
256         * E.g.: "redirect:myAction"
257         * @see RedirectView#setHttp10Compatible
258         * @see #REDIRECT_URL_PREFIX
259         */
260        public void setRedirectHttp10Compatible(boolean redirectHttp10Compatible) {
261                this.redirectHttp10Compatible = redirectHttp10Compatible;
262        }
263
264        /**
265         * Return whether redirects should stay compatible with HTTP 1.0 clients.
266         */
267        protected boolean isRedirectHttp10Compatible() {
268                return this.redirectHttp10Compatible;
269        }
270
271        /**
272         * Configure one or more hosts associated with the application.
273         * All other hosts will be considered external hosts.
274         * <p>In effect, this property provides a way turn off encoding on redirect
275         * via {@link HttpServletResponse#encodeRedirectURL} for URLs that have a
276         * host and that host is not listed as a known host.
277         * <p>If not set (the default) all URLs are encoded through the response.
278         * @param redirectHosts one or more application hosts
279         * @since 4.3
280         */
281        public void setRedirectHosts(@Nullable String... redirectHosts) {
282                this.redirectHosts = redirectHosts;
283        }
284
285        /**
286         * Return the configured application hosts for redirect purposes.
287         * @since 4.3
288         */
289        @Nullable
290        public String[] getRedirectHosts() {
291                return this.redirectHosts;
292        }
293
294        /**
295         * Set the name of the RequestContext attribute for all views.
296         * @param requestContextAttribute name of the RequestContext attribute
297         * @see AbstractView#setRequestContextAttribute
298         */
299        public void setRequestContextAttribute(@Nullable String requestContextAttribute) {
300                this.requestContextAttribute = requestContextAttribute;
301        }
302
303        /**
304         * Return the name of the RequestContext attribute for all views, if any.
305         */
306        @Nullable
307        protected String getRequestContextAttribute() {
308                return this.requestContextAttribute;
309        }
310
311        /**
312         * Set static attributes from a {@code java.util.Properties} object,
313         * for all views returned by this resolver.
314         * <p>This is the most convenient way to set static attributes. Note that
315         * static attributes can be overridden by dynamic attributes, if a value
316         * with the same name is included in the model.
317         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
318         * or a "props" element in XML bean definitions.
319         * @see org.springframework.beans.propertyeditors.PropertiesEditor
320         * @see AbstractView#setAttributes
321         */
322        public void setAttributes(Properties props) {
323                CollectionUtils.mergePropertiesIntoMap(props, this.staticAttributes);
324        }
325
326        /**
327         * Set static attributes from a Map, for all views returned by this resolver.
328         * This allows to set any kind of attribute values, for example bean references.
329         * <p>Can be populated with a "map" or "props" element in XML bean definitions.
330         * @param attributes a Map with name Strings as keys and attribute objects as values
331         * @see AbstractView#setAttributesMap
332         */
333        public void setAttributesMap(@Nullable Map<String, ?> attributes) {
334                if (attributes != null) {
335                        this.staticAttributes.putAll(attributes);
336                }
337        }
338
339        /**
340         * Allow Map access to the static attributes for views returned by
341         * this resolver, with the option to add or override specific entries.
342         * <p>Useful for specifying entries directly, for example via
343         * "attributesMap[myKey]". This is particularly useful for
344         * adding or overriding entries in child view definitions.
345         */
346        public Map<String, Object> getAttributesMap() {
347                return this.staticAttributes;
348        }
349
350        /**
351         * Specify whether views resolved by this resolver should add path variables to the model or not.
352         * <p>>The default setting is to let each View decide (see {@link AbstractView#setExposePathVariables}.
353         * However, you can use this property to override that.
354         * @param exposePathVariables
355         * <ul>
356         * <li>{@code true} - all Views resolved by this resolver will expose path variables
357         * <li>{@code false} - no Views resolved by this resolver will expose path variables
358         * <li>{@code null} - individual Views can decide for themselves (this is used by the default)
359         * </ul>
360         * @see AbstractView#setExposePathVariables
361         */
362        public void setExposePathVariables(@Nullable Boolean exposePathVariables) {
363                this.exposePathVariables = exposePathVariables;
364        }
365
366        /**
367         * Return whether views resolved by this resolver should add path variables to the model or not.
368         */
369        @Nullable
370        protected Boolean getExposePathVariables() {
371                return this.exposePathVariables;
372        }
373
374        /**
375         * Set whether to make all Spring beans in the application context accessible
376         * as request attributes, through lazy checking once an attribute gets accessed.
377         * <p>This will make all such beans accessible in plain {@code ${...}}
378         * expressions in a JSP 2.0 page, as well as in JSTL's {@code c:out}
379         * value expressions.
380         * <p>Default is "false".
381         * @see AbstractView#setExposeContextBeansAsAttributes
382         */
383        public void setExposeContextBeansAsAttributes(boolean exposeContextBeansAsAttributes) {
384                this.exposeContextBeansAsAttributes = exposeContextBeansAsAttributes;
385        }
386
387        @Nullable
388        protected Boolean getExposeContextBeansAsAttributes() {
389                return this.exposeContextBeansAsAttributes;
390        }
391
392        /**
393         * Specify the names of beans in the context which are supposed to be exposed.
394         * If this is non-null, only the specified beans are eligible for exposure as
395         * attributes.
396         * @see AbstractView#setExposedContextBeanNames
397         */
398        public void setExposedContextBeanNames(@Nullable String... exposedContextBeanNames) {
399                this.exposedContextBeanNames = exposedContextBeanNames;
400        }
401
402        @Nullable
403        protected String[] getExposedContextBeanNames() {
404                return this.exposedContextBeanNames;
405        }
406
407        /**
408         * Set the view names (or name patterns) that can be handled by this
409         * {@link org.springframework.web.servlet.ViewResolver}. View names can contain
410         * simple wildcards such that 'my*', '*Report' and '*Repo*' will all match the
411         * view name 'myReport'.
412         * @see #canHandle
413         */
414        public void setViewNames(@Nullable String... viewNames) {
415                this.viewNames = viewNames;
416        }
417
418        /**
419         * Return the view names (or name patterns) that can be handled by this
420         * {@link org.springframework.web.servlet.ViewResolver}.
421         */
422        @Nullable
423        protected String[] getViewNames() {
424                return this.viewNames;
425        }
426
427        /**
428         * Specify the order value for this ViewResolver bean.
429         * <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
430         * @see org.springframework.core.Ordered#getOrder()
431         */
432        public void setOrder(int order) {
433                this.order = order;
434        }
435
436        @Override
437        public int getOrder() {
438                return this.order;
439        }
440
441        @Override
442        protected void initApplicationContext() {
443                super.initApplicationContext();
444                if (getViewClass() == null) {
445                        throw new IllegalArgumentException("Property 'viewClass' is required");
446                }
447        }
448
449
450        /**
451         * This implementation returns just the view name,
452         * as this ViewResolver doesn't support localized resolution.
453         */
454        @Override
455        protected Object getCacheKey(String viewName, Locale locale) {
456                return viewName;
457        }
458
459        /**
460         * Overridden to implement check for "redirect:" prefix.
461         * <p>Not possible in {@code loadView}, since overridden
462         * {@code loadView} versions in subclasses might rely on the
463         * superclass always creating instances of the required view class.
464         * @see #loadView
465         * @see #requiredViewClass
466         */
467        @Override
468        protected View createView(String viewName, Locale locale) throws Exception {
469                // If this resolver is not supposed to handle the given view,
470                // return null to pass on to the next resolver in the chain.
471                if (!canHandle(viewName, locale)) {
472                        return null;
473                }
474
475                // Check for special "redirect:" prefix.
476                if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
477                        String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
478                        RedirectView view = new RedirectView(redirectUrl,
479                                        isRedirectContextRelative(), isRedirectHttp10Compatible());
480                        String[] hosts = getRedirectHosts();
481                        if (hosts != null) {
482                                view.setHosts(hosts);
483                        }
484                        return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
485                }
486
487                // Check for special "forward:" prefix.
488                if (viewName.startsWith(FORWARD_URL_PREFIX)) {
489                        String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
490                        InternalResourceView view = new InternalResourceView(forwardUrl);
491                        return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
492                }
493
494                // Else fall back to superclass implementation: calling loadView.
495                return super.createView(viewName, locale);
496        }
497
498        /**
499         * Indicates whether or not this {@link org.springframework.web.servlet.ViewResolver} can
500         * handle the supplied view name. If not, {@link #createView(String, java.util.Locale)} will
501         * return {@code null}. The default implementation checks against the configured
502         * {@link #setViewNames view names}.
503         * @param viewName the name of the view to retrieve
504         * @param locale the Locale to retrieve the view for
505         * @return whether this resolver applies to the specified view
506         * @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
507         */
508        protected boolean canHandle(String viewName, Locale locale) {
509                String[] viewNames = getViewNames();
510                return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
511        }
512
513        /**
514         * Delegates to {@code buildView} for creating a new instance of the
515         * specified view class. Applies the following Spring lifecycle methods
516         * (as supported by the generic Spring bean factory):
517         * <ul>
518         * <li>ApplicationContextAware's {@code setApplicationContext}
519         * <li>InitializingBean's {@code afterPropertiesSet}
520         * </ul>
521         * @param viewName the name of the view to retrieve
522         * @return the View instance
523         * @throws Exception if the view couldn't be resolved
524         * @see #buildView(String)
525         * @see org.springframework.context.ApplicationContextAware#setApplicationContext
526         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet
527         */
528        @Override
529        protected View loadView(String viewName, Locale locale) throws Exception {
530                AbstractUrlBasedView view = buildView(viewName);
531                View result = applyLifecycleMethods(viewName, view);
532                return (view.checkResource(locale) ? result : null);
533        }
534
535        /**
536         * Creates a new View instance of the specified view class and configures it.
537         * Does <i>not</i> perform any lookup for pre-defined View instances.
538         * <p>Spring lifecycle methods as defined by the bean container do not have to
539         * be called here; those will be applied by the {@code loadView} method
540         * after this method returns.
541         * <p>Subclasses will typically call {@code super.buildView(viewName)}
542         * first, before setting further properties themselves. {@code loadView}
543         * will then apply Spring lifecycle methods at the end of this process.
544         * @param viewName the name of the view to build
545         * @return the View instance
546         * @throws Exception if the view couldn't be resolved
547         * @see #loadView(String, java.util.Locale)
548         */
549        protected AbstractUrlBasedView buildView(String viewName) throws Exception {
550                Class<?> viewClass = getViewClass();
551                Assert.state(viewClass != null, "No view class");
552
553                AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
554                view.setUrl(getPrefix() + viewName + getSuffix());
555                view.setAttributesMap(getAttributesMap());
556
557                String contentType = getContentType();
558                if (contentType != null) {
559                        view.setContentType(contentType);
560                }
561
562                String requestContextAttribute = getRequestContextAttribute();
563                if (requestContextAttribute != null) {
564                        view.setRequestContextAttribute(requestContextAttribute);
565                }
566
567                Boolean exposePathVariables = getExposePathVariables();
568                if (exposePathVariables != null) {
569                        view.setExposePathVariables(exposePathVariables);
570                }
571                Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
572                if (exposeContextBeansAsAttributes != null) {
573                        view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
574                }
575                String[] exposedContextBeanNames = getExposedContextBeanNames();
576                if (exposedContextBeanNames != null) {
577                        view.setExposedContextBeanNames(exposedContextBeanNames);
578                }
579
580                return view;
581        }
582
583        /**
584         * Apply the containing {@link ApplicationContext}'s lifecycle methods
585         * to the given {@link View} instance, if such a context is available.
586         * @param viewName the name of the view
587         * @param view the freshly created View instance, pre-configured with
588         * {@link AbstractUrlBasedView}'s properties
589         * @return the {@link View} instance to use (either the original one
590         * or a decorated variant)
591         * @since 5.0
592         * @see #getApplicationContext()
593         * @see ApplicationContext#getAutowireCapableBeanFactory()
594         * @see org.springframework.beans.factory.config.AutowireCapableBeanFactory#initializeBean
595         */
596        protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
597                ApplicationContext context = getApplicationContext();
598                if (context != null) {
599                        Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
600                        if (initialized instanceof View) {
601                                return (View) initialized;
602                        }
603                }
604                return view;
605        }
606
607}