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}