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