001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.web.servlet.view;
018:
019: import java.util.Collections;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.Map;
023: import java.util.Properties;
024: import java.util.StringTokenizer;
025:
026: import javax.servlet.http.HttpServletRequest;
027: import javax.servlet.http.HttpServletResponse;
028:
029: import org.springframework.beans.factory.BeanNameAware;
030: import org.springframework.util.CollectionUtils;
031: import org.springframework.web.context.support.WebApplicationObjectSupport;
032: import org.springframework.web.servlet.View;
033: import org.springframework.web.servlet.support.RequestContext;
034:
035: /**
036: * Abstract base class for {@link org.springframework.web.servlet.View}
037: * implementations. Subclasses should be JavaBeans, to allow for
038: * convenient configuration as Spring-managed bean instances.
039: *
040: * <p>Provides support for static attributes, to be made available to the view,
041: * with a variety of ways to specify them. Static attributes will be merged
042: * with the given dynamic attributes (the model that the controller returned)
043: * for each render operation.
044: *
045: * <p>Extends {@link WebApplicationObjectSupport}, which will be helpful to
046: * some views. Subclasses just need to implement the actual rendering.
047: *
048: * @author Rod Johnson
049: * @author Juergen Hoeller
050: * @see #setAttributes
051: * @see #setAttributesMap
052: * @see #renderMergedOutputModel
053: */
054: public abstract class AbstractView extends WebApplicationObjectSupport
055: implements View, BeanNameAware {
056:
057: /** Default content type. Overridable as bean property. */
058: public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";
059:
060: private String beanName;
061:
062: private String contentType = DEFAULT_CONTENT_TYPE;
063:
064: private String requestContextAttribute;
065:
066: /** Map of static attributes, keyed by attribute name (String) */
067: private final Map staticAttributes = new HashMap();
068:
069: /**
070: * Set the view's name. Helpful for traceability.
071: * <p>Framework code must call this when constructing views.
072: */
073: public void setBeanName(String beanName) {
074: this .beanName = beanName;
075: }
076:
077: /**
078: * Return the view's name. Should never be <code>null</code>,
079: * if the view was correctly configured.
080: */
081: public String getBeanName() {
082: return this .beanName;
083: }
084:
085: /**
086: * Set the content type for this view.
087: * Default is "text/html;charset=ISO-8859-1".
088: * <p>May be ignored by subclasses if the view itself is assumed
089: * to set the content type, e.g. in case of JSPs.
090: */
091: public void setContentType(String contentType) {
092: this .contentType = contentType;
093: }
094:
095: /**
096: * Return the content type for this view.
097: */
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(
107: String requestContextAttribute) {
108: this .requestContextAttribute = requestContextAttribute;
109: }
110:
111: /**
112: * Return the name of the RequestContext attribute, if any.
113: */
114: public String getRequestContextAttribute() {
115: return this .requestContextAttribute;
116: }
117:
118: /**
119: * Set static attributes as a CSV string.
120: * Format is: attname0={value1},attname1={value1}
121: */
122: public void setAttributesCSV(String propString)
123: throws IllegalArgumentException {
124: if (propString != null) {
125: StringTokenizer st = new StringTokenizer(propString, ",");
126: while (st.hasMoreTokens()) {
127: String tok = st.nextToken();
128: int eqIdx = tok.indexOf("=");
129: if (eqIdx == -1) {
130: throw new IllegalArgumentException(
131: "Expected = in attributes CSV string '"
132: + propString + "'");
133: }
134: if (eqIdx >= tok.length() - 2) {
135: throw new IllegalArgumentException(
136: "At least 2 characters ([]) required in attributes CSV string '"
137: + propString + "'");
138: }
139: String name = tok.substring(0, eqIdx);
140: String value = tok.substring(eqIdx + 1);
141:
142: // Delete first and last characters of value: { and }
143: value = value.substring(1);
144: value = value.substring(0, value.length() - 1);
145:
146: addStaticAttribute(name, value);
147: }
148: }
149: }
150:
151: /**
152: * Set static attributes for this view from a
153: * <code>java.util.Properties</code> object.
154: * <p>This is the most convenient way to set static attributes. Note that
155: * static attributes can be overridden by dynamic attributes, if a value
156: * with the same name is included in the model.
157: * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
158: * or a "props" element in XML bean definitions.
159: * @see org.springframework.beans.propertyeditors.PropertiesEditor
160: */
161: public void setAttributes(Properties attributes) {
162: CollectionUtils.mergePropertiesIntoMap(attributes,
163: this .staticAttributes);
164: }
165:
166: /**
167: * Set static attributes for this view from a Map. This allows to set
168: * any kind of attribute values, for example bean references.
169: * <p>Can be populated with a "map" or "props" element in XML bean definitions.
170: * @param attributes Map with name Strings as keys and attribute objects as values
171: */
172: public void setAttributesMap(Map attributes) {
173: if (attributes != null) {
174: Iterator it = attributes.entrySet().iterator();
175: while (it.hasNext()) {
176: Map.Entry entry = (Map.Entry) it.next();
177: Object key = entry.getKey();
178: if (!(key instanceof String)) {
179: throw new IllegalArgumentException(
180: "Invalid attribute key [" + key
181: + "]: only Strings allowed");
182: }
183: addStaticAttribute((String) key, entry.getValue());
184: }
185: }
186: }
187:
188: /**
189: * Allow Map access to the static attributes of this view,
190: * with the option to add or override specific entries.
191: * <p>Useful for specifying entries directly, for example via
192: * "attributesMap[myKey]". This is particularly useful for
193: * adding or overriding entries in child view definitions.
194: */
195: public Map getAttributesMap() {
196: return this .staticAttributes;
197: }
198:
199: /**
200: * Add static data to this view, exposed in each view.
201: * <p>Must be invoked before any calls to <code>render</code>.
202: * @param name the name of the attribute to expose
203: * @param value the attribute value to expose
204: * @see #render
205: */
206: public void addStaticAttribute(String name, Object value) {
207: this .staticAttributes.put(name, value);
208: if (logger.isDebugEnabled()) {
209: logger.debug("Set static attribute with name '" + name
210: + "' and value [" + value + "] on view with name '"
211: + getBeanName() + "'");
212: }
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: */
221: public Map getStaticAttributes() {
222: return Collections.unmodifiableMap(this .staticAttributes);
223: }
224:
225: /**
226: * Prepares the view given the specified model, merging it with static
227: * attributes and a RequestContext attribute, if necessary.
228: * Delegates to renderMergedOutputModel for the actual rendering.
229: * @see #renderMergedOutputModel
230: */
231: public void render(Map model, HttpServletRequest request,
232: HttpServletResponse response) throws Exception {
233: if (logger.isDebugEnabled()) {
234: logger
235: .debug("Rendering view with name '" + this .beanName
236: + "' with model " + model
237: + " and static attributes "
238: + this .staticAttributes);
239: }
240:
241: // Consolidate static and dynamic model attributes.
242: Map mergedModel = new HashMap(this .staticAttributes.size()
243: + (model != null ? model.size() : 0));
244: mergedModel.putAll(this .staticAttributes);
245: if (model != null) {
246: mergedModel.putAll(model);
247: }
248:
249: // Expose RequestContext?
250: if (this .requestContextAttribute != null) {
251: mergedModel.put(this .requestContextAttribute,
252: createRequestContext(request, mergedModel));
253: }
254:
255: renderMergedOutputModel(mergedModel, request, response);
256: }
257:
258: /**
259: * Create a RequestContext to expose under the specified attribute name.
260: * <p>Default implementation creates a standard RequestContext instance for the
261: * given request and model. Can be overridden in subclasses for custom instances.
262: * @param request current HTTP request
263: * @param model combined output Map (never <code>null</code>),
264: * with dynamic values taking precedence over static attributes
265: * @return the RequestContext instance
266: * @see #setRequestContextAttribute
267: * @see org.springframework.web.servlet.support.RequestContext
268: */
269: protected RequestContext createRequestContext(
270: HttpServletRequest request, Map model) {
271: return new RequestContext(request, getServletContext(), model);
272: }
273:
274: /**
275: * Subclasses must implement this method to actually render the view.
276: * <p>The first step will be preparing the request: In the JSP case,
277: * this would mean setting model objects as request attributes.
278: * The second step will be the actual rendering of the view,
279: * for example including the JSP via a RequestDispatcher.
280: * @param model combined output Map (never <code>null</code>),
281: * with dynamic values taking precedence over static attributes
282: * @param request current HTTP request
283: * @param response current HTTP response
284: * @throws Exception if rendering failed
285: */
286: protected abstract void renderMergedOutputModel(Map model,
287: HttpServletRequest request, HttpServletResponse response)
288: throws Exception;
289:
290: /**
291: * Expose the model objects in the given map as request attributes.
292: * Names will be taken from the model Map.
293: * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}.
294: * @param model Map of model objects to expose
295: * @param request current HTTP request
296: */
297: protected void exposeModelAsRequestAttributes(Map model,
298: HttpServletRequest request) throws Exception {
299: Iterator it = model.entrySet().iterator();
300: while (it.hasNext()) {
301: Map.Entry entry = (Map.Entry) it.next();
302: if (!(entry.getKey() instanceof String)) {
303: throw new IllegalArgumentException(
304: "Invalid key ["
305: + entry.getKey()
306: + "] in model Map: only Strings allowed as model keys");
307: }
308: String modelName = (String) entry.getKey();
309: Object modelValue = entry.getValue();
310: if (modelValue != null) {
311: request.setAttribute(modelName, modelValue);
312: if (logger.isDebugEnabled()) {
313: logger.debug("Added model object '" + modelName
314: + "' of type ["
315: + modelValue.getClass().getName()
316: + "] to request in view with name '"
317: + getBeanName() + "'");
318: }
319: } else {
320: request.removeAttribute(modelName);
321: if (logger.isDebugEnabled()) {
322: logger.debug("Removed model object '" + modelName
323: + "' from request in view with name '"
324: + getBeanName() + "'");
325: }
326: }
327: }
328: }
329:
330: public String toString() {
331: StringBuffer sb = new StringBuffer(getClass().getName());
332: if (getBeanName() != null) {
333: sb.append(": name '").append(getBeanName()).append("'");
334: } else {
335: sb.append(": unnamed");
336: }
337: return sb.toString();
338: }
339:
340: }
|