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.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.util.Arrays; 022import java.util.Collections; 023import java.util.HashSet; 024import java.util.LinkedHashMap; 025import java.util.Map; 026import java.util.Properties; 027import java.util.Set; 028import java.util.StringTokenizer; 029 030import javax.servlet.ServletOutputStream; 031import javax.servlet.http.HttpServletRequest; 032import javax.servlet.http.HttpServletResponse; 033 034import org.springframework.beans.factory.BeanNameAware; 035import org.springframework.http.MediaType; 036import org.springframework.lang.Nullable; 037import org.springframework.util.Assert; 038import org.springframework.util.CollectionUtils; 039import org.springframework.web.context.WebApplicationContext; 040import org.springframework.web.context.support.ContextExposingHttpServletRequest; 041import org.springframework.web.context.support.WebApplicationObjectSupport; 042import org.springframework.web.servlet.View; 043import org.springframework.web.servlet.support.RequestContext; 044 045/** 046 * Abstract base class for {@link org.springframework.web.servlet.View} 047 * implementations. Subclasses should be JavaBeans, to allow for 048 * convenient configuration as Spring-managed bean instances. 049 * 050 * <p>Provides support for static attributes, to be made available to the view, 051 * with a variety of ways to specify them. Static attributes will be merged 052 * with the given dynamic attributes (the model that the controller returned) 053 * for each render operation. 054 * 055 * <p>Extends {@link WebApplicationObjectSupport}, which will be helpful to 056 * some views. Subclasses just need to implement the actual rendering. 057 * 058 * @author Rod Johnson 059 * @author Juergen Hoeller 060 * @see #setAttributes 061 * @see #setAttributesMap 062 * @see #renderMergedOutputModel 063 */ 064public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware { 065 066 /** Default content type. Overridable as bean property. */ 067 public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1"; 068 069 /** Initial size for the temporary output byte array (if any). */ 070 private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096; 071 072 073 @Nullable 074 private String contentType = DEFAULT_CONTENT_TYPE; 075 076 @Nullable 077 private String requestContextAttribute; 078 079 private final Map<String, Object> staticAttributes = new LinkedHashMap<>(); 080 081 private boolean exposePathVariables = true; 082 083 private boolean exposeContextBeansAsAttributes = false; 084 085 @Nullable 086 private Set<String> exposedContextBeanNames; 087 088 @Nullable 089 private String beanName; 090 091 092 093 /** 094 * Set the content type for this view. 095 * Default is "text/html;charset=ISO-8859-1". 096 * <p>May be ignored by subclasses if the view itself is assumed 097 * to set the content type, e.g. in case of JSPs. 098 */ 099 public void setContentType(@Nullable String contentType) { 100 this.contentType = contentType; 101 } 102 103 /** 104 * Return the content type for this view. 105 */ 106 @Override 107 @Nullable 108 public String getContentType() { 109 return this.contentType; 110 } 111 112 /** 113 * Set the name of the RequestContext attribute for this view. 114 * Default is none. 115 */ 116 public void setRequestContextAttribute(@Nullable String requestContextAttribute) { 117 this.requestContextAttribute = requestContextAttribute; 118 } 119 120 /** 121 * Return the name of the RequestContext attribute, if any. 122 */ 123 @Nullable 124 public String getRequestContextAttribute() { 125 return this.requestContextAttribute; 126 } 127 128 /** 129 * Set static attributes as a CSV string. 130 * Format is: attname0={value1},attname1={value1} 131 * <p>"Static" attributes are fixed attributes that are specified in 132 * the View instance configuration. "Dynamic" attributes, on the other hand, 133 * are values passed in as part of the model. 134 */ 135 public void setAttributesCSV(@Nullable String propString) throws IllegalArgumentException { 136 if (propString != null) { 137 StringTokenizer st = new StringTokenizer(propString, ","); 138 while (st.hasMoreTokens()) { 139 String tok = st.nextToken(); 140 int eqIdx = tok.indexOf('='); 141 if (eqIdx == -1) { 142 throw new IllegalArgumentException( 143 "Expected '=' in attributes CSV string '" + propString + "'"); 144 } 145 if (eqIdx >= tok.length() - 2) { 146 throw new IllegalArgumentException( 147 "At least 2 characters ([]) required in attributes CSV string '" + propString + "'"); 148 } 149 String name = tok.substring(0, eqIdx); 150 // Delete first and last characters of value: { and } 151 int beginIndex = eqIdx + 2; 152 int endIndex = tok.length() - 1; 153 String value = tok.substring(beginIndex, endIndex); 154 155 addStaticAttribute(name, value); 156 } 157 } 158 } 159 160 /** 161 * Set static attributes for this view from a 162 * {@code java.util.Properties} object. 163 * <p>"Static" attributes are fixed attributes that are specified in 164 * the View instance configuration. "Dynamic" attributes, on the other hand, 165 * are values passed in as part of the model. 166 * <p>This is the most convenient way to set static attributes. Note that 167 * static attributes can be overridden by dynamic attributes, if a value 168 * with the same name is included in the model. 169 * <p>Can be populated with a String "value" (parsed via PropertiesEditor) 170 * or a "props" element in XML bean definitions. 171 * @see org.springframework.beans.propertyeditors.PropertiesEditor 172 */ 173 public void setAttributes(Properties attributes) { 174 CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes); 175 } 176 177 /** 178 * Set static attributes for this view from a Map. This allows to set 179 * any kind of attribute values, for example bean references. 180 * <p>"Static" attributes are fixed attributes that are specified in 181 * the View instance configuration. "Dynamic" attributes, on the other hand, 182 * are values passed in as part of the model. 183 * <p>Can be populated with a "map" or "props" element in XML bean definitions. 184 * @param attributes a Map with name Strings as keys and attribute objects as values 185 */ 186 public void setAttributesMap(@Nullable Map<String, ?> attributes) { 187 if (attributes != null) { 188 attributes.forEach(this::addStaticAttribute); 189 } 190 } 191 192 /** 193 * Allow Map access to the static attributes of this view, 194 * with the option to add or override specific entries. 195 * <p>Useful for specifying entries directly, for example via 196 * "attributesMap[myKey]". This is particularly useful for 197 * adding or overriding entries in child view definitions. 198 */ 199 public Map<String, Object> getAttributesMap() { 200 return this.staticAttributes; 201 } 202 203 /** 204 * Add static data to this view, exposed in each view. 205 * <p>"Static" attributes are fixed attributes that are specified in 206 * the View instance configuration. "Dynamic" attributes, on the other hand, 207 * are values passed in as part of the model. 208 * <p>Must be invoked before any calls to {@code render}. 209 * @param name the name of the attribute to expose 210 * @param value the attribute value to expose 211 * @see #render 212 */ 213 public void addStaticAttribute(String name, Object value) { 214 this.staticAttributes.put(name, value); 215 } 216 217 /** 218 * Return the static attributes for this view. Handy for testing. 219 * <p>Returns an unmodifiable Map, as this is not intended for 220 * manipulating the Map but rather just for checking the contents. 221 * @return the static attributes in this view 222 */ 223 public Map<String, Object> getStaticAttributes() { 224 return Collections.unmodifiableMap(this.staticAttributes); 225 } 226 227 /** 228 * Specify whether to add path variables to the model or not. 229 * <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable} 230 * annotation. They're are effectively URI template variables with type conversion applied to 231 * them to derive typed Object values. Such values are frequently needed in views for 232 * constructing links to the same and other URLs. 233 * <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)}) 234 * but not attributes already present in the model. 235 * <p>By default this flag is set to {@code true}. Concrete view types can override this. 236 * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise 237 */ 238 public void setExposePathVariables(boolean exposePathVariables) { 239 this.exposePathVariables = exposePathVariables; 240 } 241 242 /** 243 * Return whether to add path variables to the model or not. 244 */ 245 public boolean isExposePathVariables() { 246 return this.exposePathVariables; 247 } 248 249 /** 250 * Set whether to make all Spring beans in the application context accessible 251 * as request attributes, through lazy checking once an attribute gets accessed. 252 * <p>This will make all such beans accessible in plain {@code ${...}} 253 * expressions in a JSP 2.0 page, as well as in JSTL's {@code c:out} 254 * value expressions. 255 * <p>Default is "false". Switch this flag on to transparently expose all 256 * Spring beans in the request attribute namespace. 257 * <p><b>NOTE:</b> Context beans will override any custom request or session 258 * attributes of the same name that have been manually added. However, model 259 * attributes (as explicitly exposed to this view) of the same name will 260 * always override context beans. 261 * @see #getRequestToExpose 262 */ 263 public void setExposeContextBeansAsAttributes(boolean exposeContextBeansAsAttributes) { 264 this.exposeContextBeansAsAttributes = exposeContextBeansAsAttributes; 265 } 266 267 /** 268 * Specify the names of beans in the context which are supposed to be exposed. 269 * If this is non-null, only the specified beans are eligible for exposure as 270 * attributes. 271 * <p>If you'd like to expose all Spring beans in the application context, switch 272 * the {@link #setExposeContextBeansAsAttributes "exposeContextBeansAsAttributes"} 273 * flag on but do not list specific bean names for this property. 274 */ 275 public void setExposedContextBeanNames(String... exposedContextBeanNames) { 276 this.exposedContextBeanNames = new HashSet<>(Arrays.asList(exposedContextBeanNames)); 277 } 278 279 /** 280 * Set the view's name. Helpful for traceability. 281 * <p>Framework code must call this when constructing views. 282 */ 283 @Override 284 public void setBeanName(@Nullable String beanName) { 285 this.beanName = beanName; 286 } 287 288 /** 289 * Return the view's name. Should never be {@code null}, 290 * if the view was correctly configured. 291 */ 292 @Nullable 293 public String getBeanName() { 294 return this.beanName; 295 } 296 297 298 /** 299 * Prepares the view given the specified model, merging it with static 300 * attributes and a RequestContext attribute, if necessary. 301 * Delegates to renderMergedOutputModel for the actual rendering. 302 * @see #renderMergedOutputModel 303 */ 304 @Override 305 public void render(@Nullable Map<String, ?> model, HttpServletRequest request, 306 HttpServletResponse response) throws Exception { 307 308 if (logger.isDebugEnabled()) { 309 logger.debug("View " + formatViewName() + 310 ", model " + (model != null ? model : Collections.emptyMap()) + 311 (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes)); 312 } 313 314 Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); 315 prepareResponse(request, response); 316 renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); 317 } 318 319 /** 320 * Creates a combined output Map (never {@code null}) that includes dynamic values and static attributes. 321 * Dynamic values take precedence over static attributes. 322 */ 323 protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model, 324 HttpServletRequest request, HttpServletResponse response) { 325 326 @SuppressWarnings("unchecked") 327 Map<String, Object> pathVars = (this.exposePathVariables ? 328 (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null); 329 330 // Consolidate static and dynamic model attributes. 331 int size = this.staticAttributes.size(); 332 size += (model != null ? model.size() : 0); 333 size += (pathVars != null ? pathVars.size() : 0); 334 335 Map<String, Object> mergedModel = new LinkedHashMap<>(size); 336 mergedModel.putAll(this.staticAttributes); 337 if (pathVars != null) { 338 mergedModel.putAll(pathVars); 339 } 340 if (model != null) { 341 mergedModel.putAll(model); 342 } 343 344 // Expose RequestContext? 345 if (this.requestContextAttribute != null) { 346 mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); 347 } 348 349 return mergedModel; 350 } 351 352 /** 353 * Create a RequestContext to expose under the specified attribute name. 354 * <p>The default implementation creates a standard RequestContext instance for the 355 * given request and model. Can be overridden in subclasses for custom instances. 356 * @param request current HTTP request 357 * @param model combined output Map (never {@code null}), 358 * with dynamic values taking precedence over static attributes 359 * @return the RequestContext instance 360 * @see #setRequestContextAttribute 361 * @see org.springframework.web.servlet.support.RequestContext 362 */ 363 protected RequestContext createRequestContext( 364 HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) { 365 366 return new RequestContext(request, response, getServletContext(), model); 367 } 368 369 /** 370 * Prepare the given response for rendering. 371 * <p>The default implementation applies a workaround for an IE bug 372 * when sending download content via HTTPS. 373 * @param request current HTTP request 374 * @param response current HTTP response 375 */ 376 protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { 377 if (generatesDownloadContent()) { 378 response.setHeader("Pragma", "private"); 379 response.setHeader("Cache-Control", "private, must-revalidate"); 380 } 381 } 382 383 /** 384 * Return whether this view generates download content 385 * (typically binary content like PDF or Excel files). 386 * <p>The default implementation returns {@code false}. Subclasses are 387 * encouraged to return {@code true} here if they know that they are 388 * generating download content that requires temporary caching on the 389 * client side, typically via the response OutputStream. 390 * @see #prepareResponse 391 * @see javax.servlet.http.HttpServletResponse#getOutputStream() 392 */ 393 protected boolean generatesDownloadContent() { 394 return false; 395 } 396 397 /** 398 * Get the request handle to expose to {@link #renderMergedOutputModel}, i.e. to the view. 399 * <p>The default implementation wraps the original request for exposure of Spring beans 400 * as request attributes (if demanded). 401 * @param originalRequest the original servlet request as provided by the engine 402 * @return the wrapped request, or the original request if no wrapping is necessary 403 * @see #setExposeContextBeansAsAttributes 404 * @see #setExposedContextBeanNames 405 * @see org.springframework.web.context.support.ContextExposingHttpServletRequest 406 */ 407 protected HttpServletRequest getRequestToExpose(HttpServletRequest originalRequest) { 408 if (this.exposeContextBeansAsAttributes || this.exposedContextBeanNames != null) { 409 WebApplicationContext wac = getWebApplicationContext(); 410 Assert.state(wac != null, "No WebApplicationContext"); 411 return new ContextExposingHttpServletRequest(originalRequest, wac, this.exposedContextBeanNames); 412 } 413 return originalRequest; 414 } 415 416 /** 417 * Subclasses must implement this method to actually render the view. 418 * <p>The first step will be preparing the request: In the JSP case, 419 * this would mean setting model objects as request attributes. 420 * The second step will be the actual rendering of the view, 421 * for example including the JSP via a RequestDispatcher. 422 * @param model combined output Map (never {@code null}), 423 * with dynamic values taking precedence over static attributes 424 * @param request current HTTP request 425 * @param response current HTTP response 426 * @throws Exception if rendering failed 427 */ 428 protected abstract void renderMergedOutputModel( 429 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception; 430 431 432 /** 433 * Expose the model objects in the given map as request attributes. 434 * Names will be taken from the model Map. 435 * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}. 436 * @param model a Map of model objects to expose 437 * @param request current HTTP request 438 */ 439 protected void exposeModelAsRequestAttributes(Map<String, Object> model, 440 HttpServletRequest request) throws Exception { 441 442 model.forEach((name, value) -> { 443 if (value != null) { 444 request.setAttribute(name, value); 445 } 446 else { 447 request.removeAttribute(name); 448 } 449 }); 450 } 451 452 /** 453 * Create a temporary OutputStream for this view. 454 * <p>This is typically used as IE workaround, for setting the content length header 455 * from the temporary stream before actually writing the content to the HTTP response. 456 */ 457 protected ByteArrayOutputStream createTemporaryOutputStream() { 458 return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); 459 } 460 461 /** 462 * Write the given temporary OutputStream to the HTTP response. 463 * @param response current HTTP response 464 * @param baos the temporary OutputStream to write 465 * @throws IOException if writing/flushing failed 466 */ 467 protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException { 468 // Write content type and also length (determined via byte array). 469 response.setContentType(getContentType()); 470 response.setContentLength(baos.size()); 471 472 // Flush byte array to servlet output stream. 473 ServletOutputStream out = response.getOutputStream(); 474 baos.writeTo(out); 475 out.flush(); 476 } 477 478 /** 479 * Set the content type of the response to the configured 480 * {@link #setContentType(String) content type} unless the 481 * {@link View#SELECTED_CONTENT_TYPE} request attribute is present and set 482 * to a concrete media type. 483 */ 484 protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) { 485 MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE); 486 if (mediaType != null && mediaType.isConcrete()) { 487 response.setContentType(mediaType.toString()); 488 } 489 else { 490 response.setContentType(getContentType()); 491 } 492 } 493 494 @Override 495 public String toString() { 496 return getClass().getName() + ": " + formatViewName(); 497 } 498 499 protected String formatViewName() { 500 return (getBeanName() != null ? "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]"); 501 } 502 503}