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