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