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}