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}