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