1 /* 2 * Copyright 2002-2009 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.web.servlet.view; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.IOException; 21 import java.util.Collections; 22 import java.util.HashMap; 23 import java.util.Map; 24 import java.util.Properties; 25 import java.util.StringTokenizer; 26 import javax.servlet.ServletOutputStream; 27 import javax.servlet.http.HttpServletRequest; 28 import javax.servlet.http.HttpServletResponse; 29 30 import org.springframework.beans.factory.BeanNameAware; 31 import org.springframework.util.CollectionUtils; 32 import org.springframework.web.context.support.WebApplicationObjectSupport; 33 import org.springframework.web.servlet.View; 34 import org.springframework.web.servlet.support.RequestContext; 35 36 /** 37 * Abstract base class for {@link org.springframework.web.servlet.View} 38 * implementations. Subclasses should be JavaBeans, to allow for 39 * convenient configuration as Spring-managed bean instances. 40 * 41 * <p>Provides support for static attributes, to be made available to the view, 42 * with a variety of ways to specify them. Static attributes will be merged 43 * with the given dynamic attributes (the model that the controller returned) 44 * for each render operation. 45 * 46 * <p>Extends {@link WebApplicationObjectSupport}, which will be helpful to 47 * some views. Subclasses just need to implement the actual rendering. 48 * 49 * @author Rod Johnson 50 * @author Juergen Hoeller 51 * @see #setAttributes 52 * @see #setAttributesMap 53 * @see #renderMergedOutputModel 54 */ 55 public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware { 56 57 /** Default content type. Overridable as bean property. */ 58 public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1"; 59 60 /** Initial size for the temporary output byte array (if any) */ 61 private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096; 62 63 64 private String beanName; 65 66 private String contentType = DEFAULT_CONTENT_TYPE; 67 68 private String requestContextAttribute; 69 70 /** Map of static attributes, keyed by attribute name (String) */ 71 private final Map<String, Object> staticAttributes = new HashMap<String, Object>(); 72 73 /** Whether or not the view should add path variables in the model */ 74 private boolean exposePathVariables = true; 75 76 /** 77 * Set the view's name. Helpful for traceability. 78 * <p>Framework code must call this when constructing views. 79 */ setBeanName(String beanName)80 public void setBeanName(String beanName) { 81 this.beanName = beanName; 82 } 83 84 /** 85 * Return the view's name. Should never be <code>null</code>, 86 * if the view was correctly configured. 87 */ getBeanName()88 public String getBeanName() { 89 return this.beanName; 90 } 91 92 /** 93 * Set the content type for this view. 94 * Default is "text/html;charset=ISO-8859-1". 95 * <p>May be ignored by subclasses if the view itself is assumed 96 * to set the content type, e.g. in case of JSPs. 97 */ setContentType(String contentType)98 public void setContentType(String contentType) { 99 this.contentType = contentType; 100 } 101 102 /** 103 * Return the content type for this view. 104 */ getContentType()105 public String getContentType() { 106 return this.contentType; 107 } 108 109 /** 110 * Set the name of the RequestContext attribute for this view. 111 * Default is none. 112 */ setRequestContextAttribute(String requestContextAttribute)113 public void setRequestContextAttribute(String requestContextAttribute) { 114 this.requestContextAttribute = requestContextAttribute; 115 } 116 117 /** 118 * Return the name of the RequestContext attribute, if any. 119 */ getRequestContextAttribute()120 public String getRequestContextAttribute() { 121 return this.requestContextAttribute; 122 } 123 124 /** 125 * Set static attributes as a CSV string. 126 * Format is: attname0={value1},attname1={value1} 127 * <p>"Static" attributes are fixed attributes that are specified in 128 * the View instance configuration. "Dynamic" attributes, on the other hand, 129 * are values passed in as part of the model. 130 */ setAttributesCSV(String propString)131 public void setAttributesCSV(String propString) throws IllegalArgumentException { 132 if (propString != null) { 133 StringTokenizer st = new StringTokenizer(propString, ","); 134 while (st.hasMoreTokens()) { 135 String tok = st.nextToken(); 136 int eqIdx = tok.indexOf("="); 137 if (eqIdx == -1) { 138 throw new IllegalArgumentException("Expected = in attributes CSV string '" + propString + "'"); 139 } 140 if (eqIdx >= tok.length() - 2) { 141 throw new IllegalArgumentException( 142 "At least 2 characters ([]) required in attributes CSV string '" + propString + "'"); 143 } 144 String name = tok.substring(0, eqIdx); 145 String value = tok.substring(eqIdx + 1); 146 147 // Delete first and last characters of value: { and } 148 value = value.substring(1); 149 value = value.substring(0, value.length() - 1); 150 151 addStaticAttribute(name, value); 152 } 153 } 154 } 155 156 /** 157 * Set static attributes for this view from a 158 * <code>java.util.Properties</code> object. 159 * <p>"Static" attributes are fixed attributes that are specified in 160 * the View instance configuration. "Dynamic" attributes, on the other hand, 161 * are values passed in as part of the model. 162 * <p>This is the most convenient way to set static attributes. Note that 163 * static attributes can be overridden by dynamic attributes, if a value 164 * with the same name is included in the model. 165 * <p>Can be populated with a String "value" (parsed via PropertiesEditor) 166 * or a "props" element in XML bean definitions. 167 * @see org.springframework.beans.propertyeditors.PropertiesEditor 168 */ setAttributes(Properties attributes)169 public void setAttributes(Properties attributes) { 170 CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes); 171 } 172 173 /** 174 * Set static attributes for this view from a Map. This allows to set 175 * any kind of attribute values, for example bean references. 176 * <p>"Static" attributes are fixed attributes that are specified in 177 * the View instance configuration. "Dynamic" attributes, on the other hand, 178 * are values passed in as part of the model. 179 * <p>Can be populated with a "map" or "props" element in XML bean definitions. 180 * @param attributes Map with name Strings as keys and attribute objects as values 181 */ setAttributesMap(Map<String, ?> attributes)182 public void setAttributesMap(Map<String, ?> attributes) { 183 if (attributes != null) { 184 for (Map.Entry<String, ?> entry : attributes.entrySet()) { 185 addStaticAttribute(entry.getKey(), entry.getValue()); 186 } 187 } 188 } 189 190 /** 191 * Allow Map access to the static attributes of this view, 192 * with the option to add or override specific entries. 193 * <p>Useful for specifying entries directly, for example via 194 * "attributesMap[myKey]". This is particularly useful for 195 * adding or overriding entries in child view definitions. 196 */ getAttributesMap()197 public Map<String, Object> getAttributesMap() { 198 return this.staticAttributes; 199 } 200 201 /** 202 * Add static data to this view, exposed in each view. 203 * <p>"Static" attributes are fixed attributes that are specified in 204 * the View instance configuration. "Dynamic" attributes, on the other hand, 205 * are values passed in as part of the model. 206 * <p>Must be invoked before any calls to <code>render</code>. 207 * @param name the name of the attribute to expose 208 * @param value the attribute value to expose 209 * @see #render 210 */ addStaticAttribute(String name, Object value)211 public void addStaticAttribute(String name, Object value) { 212 this.staticAttributes.put(name, value); 213 } 214 215 /** 216 * Return the static attributes for this view. Handy for testing. 217 * <p>Returns an unmodifiable Map, as this is not intended for 218 * manipulating the Map but rather just for checking the contents. 219 * @return the static attributes in this view 220 */ getStaticAttributes()221 public Map<String, Object> getStaticAttributes() { 222 return Collections.unmodifiableMap(this.staticAttributes); 223 } 224 225 /** 226 * Whether to add path variables in the model or not. 227 * <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable} 228 * annotation. They're are effectively URI template variables with type conversion applied to 229 * them to derive typed Object values. Such values are frequently needed in views for 230 * constructing links to the same and other URLs. 231 * <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)}) 232 * but not attributes already present in the model. 233 * <p>By default this flag is set to {@code true}. Concrete view types can override this. 234 * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise. 235 */ setExposePathVariables(boolean exposePathVariables)236 public void setExposePathVariables(boolean exposePathVariables) { 237 this.exposePathVariables = exposePathVariables; 238 } 239 240 /** 241 * Returns the value of the flag indicating whether path variables should be added to the model or not. 242 */ isExposePathVariables()243 public boolean isExposePathVariables() { 244 return exposePathVariables; 245 } 246 247 /** 248 * Prepares the view given the specified model, merging it with static 249 * attributes and a RequestContext attribute, if necessary. 250 * Delegates to renderMergedOutputModel for the actual rendering. 251 * @see #renderMergedOutputModel 252 */ render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)253 public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { 254 if (logger.isTraceEnabled()) { 255 logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + 256 " and static attributes " + this.staticAttributes); 257 } 258 259 Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); 260 261 prepareResponse(request, response); 262 renderMergedOutputModel(mergedModel, request, response); 263 } 264 265 /** 266 * Creates a combined output Map (never <code>null</code>) that includes dynamic values and static attributes. 267 * Dynamic values take precedence over static attributes. 268 */ createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)269 protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, 270 HttpServletResponse response) { 271 @SuppressWarnings("unchecked") 272 Map<String, Object> pathVars = this.exposePathVariables ? 273 (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null; 274 275 // Consolidate static and dynamic model attributes. 276 int size = this.staticAttributes.size(); 277 size += (model != null) ? model.size() : 0; 278 size += (pathVars != null) ? pathVars.size() : 0; 279 Map<String, Object> mergedModel = new HashMap<String, Object>(size); 280 mergedModel.putAll(this.staticAttributes); 281 if (pathVars != null) { 282 mergedModel.putAll(pathVars); 283 } 284 if (model != null) { 285 mergedModel.putAll(model); 286 } 287 288 // Expose RequestContext? 289 if (this.requestContextAttribute != null) { 290 mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); 291 } 292 293 return mergedModel; 294 } 295 296 /** 297 * Create a RequestContext to expose under the specified attribute name. 298 * <p>Default implementation creates a standard RequestContext instance for the 299 * given request and model. Can be overridden in subclasses for custom instances. 300 * @param request current HTTP request 301 * @param model combined output Map (never <code>null</code>), 302 * with dynamic values taking precedence over static attributes 303 * @return the RequestContext instance 304 * @see #setRequestContextAttribute 305 * @see org.springframework.web.servlet.support.RequestContext 306 */ createRequestContext( HttpServletRequest request, HttpServletResponse response, Map<String, Object> model)307 protected RequestContext createRequestContext( 308 HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) { 309 310 return new RequestContext(request, response, getServletContext(), model); 311 } 312 313 /** 314 * Prepare the given response for rendering. 315 * <p>The default implementation applies a workaround for an IE bug 316 * when sending download content via HTTPS. 317 * @param request current HTTP request 318 * @param response current HTTP response 319 */ prepareResponse(HttpServletRequest request, HttpServletResponse response)320 protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { 321 if (generatesDownloadContent()) { 322 response.setHeader("Pragma", "private"); 323 response.setHeader("Cache-Control", "private, must-revalidate"); 324 } 325 } 326 327 /** 328 * Return whether this view generates download content 329 * (typically binary content like PDF or Excel files). 330 * <p>The default implementation returns <code>false</code>. Subclasses are 331 * encouraged to return <code>true</code> here if they know that they are 332 * generating download content that requires temporary caching on the 333 * client side, typically via the response OutputStream. 334 * @see #prepareResponse 335 * @see javax.servlet.http.HttpServletResponse#getOutputStream() 336 */ generatesDownloadContent()337 protected boolean generatesDownloadContent() { 338 return false; 339 } 340 341 /** 342 * Subclasses must implement this method to actually render the view. 343 * <p>The first step will be preparing the request: In the JSP case, 344 * this would mean setting model objects as request attributes. 345 * The second step will be the actual rendering of the view, 346 * for example including the JSP via a RequestDispatcher. 347 * @param model combined output Map (never <code>null</code>), 348 * with dynamic values taking precedence over static attributes 349 * @param request current HTTP request 350 * @param response current HTTP response 351 * @throws Exception if rendering failed 352 */ renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)353 protected abstract void renderMergedOutputModel( 354 Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception; 355 356 357 /** 358 * Expose the model objects in the given map as request attributes. 359 * Names will be taken from the model Map. 360 * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}. 361 * @param model Map of model objects to expose 362 * @param request current HTTP request 363 */ exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request)364 protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { 365 for (Map.Entry<String, Object> entry : model.entrySet()) { 366 String modelName = entry.getKey(); 367 Object modelValue = entry.getValue(); 368 if (modelValue != null) { 369 request.setAttribute(modelName, modelValue); 370 if (logger.isDebugEnabled()) { 371 logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() + 372 "] to request in view with name '" + getBeanName() + "'"); 373 } 374 } 375 else { 376 request.removeAttribute(modelName); 377 if (logger.isDebugEnabled()) { 378 logger.debug("Removed model object '" + modelName + 379 "' from request in view with name '" + getBeanName() + "'"); 380 } 381 } 382 } 383 } 384 385 /** 386 * Create a temporary OutputStream for this view. 387 * <p>This is typically used as IE workaround, for setting the content length header 388 * from the temporary stream before actually writing the content to the HTTP response. 389 */ createTemporaryOutputStream()390 protected ByteArrayOutputStream createTemporaryOutputStream() { 391 return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); 392 } 393 394 /** 395 * Write the given temporary OutputStream to the HTTP response. 396 * @param response current HTTP response 397 * @param baos the temporary OutputStream to write 398 * @throws IOException if writing/flushing failed 399 */ writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos)400 protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException { 401 // Write content type and also length (determined via byte array). 402 response.setContentType(getContentType()); 403 response.setContentLength(baos.size()); 404 405 // Flush byte array to servlet output stream. 406 ServletOutputStream out = response.getOutputStream(); 407 baos.writeTo(out); 408 out.flush(); 409 } 410 411 412 @Override toString()413 public String toString() { 414 StringBuilder sb = new StringBuilder(getClass().getName()); 415 if (getBeanName() != null) { 416 sb.append(": name '").append(getBeanName()).append("'"); 417 } 418 else { 419 sb.append(": unnamed"); 420 } 421 return sb.toString(); 422 } 423 424 } 425