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;
029
030import javax.servlet.ServletOutputStream;
031import javax.servlet.http.HttpServletRequest;
032import javax.servlet.http.HttpServletResponse;
033
034import org.springframework.beans.factory.BeanNameAware;
035import org.springframework.http.MediaType;
036import org.springframework.lang.Nullable;
037import org.springframework.util.Assert;
038import org.springframework.util.CollectionUtils;
039import org.springframework.web.context.WebApplicationContext;
040import org.springframework.web.context.support.ContextExposingHttpServletRequest;
041import org.springframework.web.context.support.WebApplicationObjectSupport;
042import org.springframework.web.servlet.View;
043import org.springframework.web.servlet.support.RequestContext;
044
045/**
046 * Abstract base class for {@link org.springframework.web.servlet.View}
047 * implementations. Subclasses should be JavaBeans, to allow for
048 * convenient configuration as Spring-managed bean instances.
049 *
050 * <p>Provides support for static attributes, to be made available to the view,
051 * with a variety of ways to specify them. Static attributes will be merged
052 * with the given dynamic attributes (the model that the controller returned)
053 * for each render operation.
054 *
055 * <p>Extends {@link WebApplicationObjectSupport}, which will be helpful to
056 * some views. Subclasses just need to implement the actual rendering.
057 *
058 * @author Rod Johnson
059 * @author Juergen Hoeller
060 * @see #setAttributes
061 * @see #setAttributesMap
062 * @see #renderMergedOutputModel
063 */
064public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {
065
066        /** Default content type. Overridable as bean property. */
067        public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";
068
069        /** Initial size for the temporary output byte array (if any). */
070        private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096;
071
072
073        @Nullable
074        private String contentType = DEFAULT_CONTENT_TYPE;
075
076        @Nullable
077        private String requestContextAttribute;
078
079        private final Map<String, Object> staticAttributes = new LinkedHashMap<>();
080
081        private boolean exposePathVariables = true;
082
083        private boolean exposeContextBeansAsAttributes = false;
084
085        @Nullable
086        private Set<String> exposedContextBeanNames;
087
088        @Nullable
089        private String beanName;
090
091
092
093        /**
094         * Set the content type for this view.
095         * Default is "text/html;charset=ISO-8859-1".
096         * <p>May be ignored by subclasses if the view itself is assumed
097         * to set the content type, e.g. in case of JSPs.
098         */
099        public void setContentType(@Nullable String contentType) {
100                this.contentType = contentType;
101        }
102
103        /**
104         * Return the content type for this view.
105         */
106        @Override
107        @Nullable
108        public String getContentType() {
109                return this.contentType;
110        }
111
112        /**
113         * Set the name of the RequestContext attribute for this view.
114         * Default is none.
115         */
116        public void setRequestContextAttribute(@Nullable String requestContextAttribute) {
117                this.requestContextAttribute = requestContextAttribute;
118        }
119
120        /**
121         * Return the name of the RequestContext attribute, if any.
122         */
123        @Nullable
124        public String getRequestContextAttribute() {
125                return this.requestContextAttribute;
126        }
127
128        /**
129         * Set static attributes as a CSV string.
130         * Format is: attname0={value1},attname1={value1}
131         * <p>"Static" attributes are fixed attributes that are specified in
132         * the View instance configuration. "Dynamic" attributes, on the other hand,
133         * are values passed in as part of the model.
134         */
135        public void setAttributesCSV(@Nullable String propString) throws IllegalArgumentException {
136                if (propString != null) {
137                        StringTokenizer st = new StringTokenizer(propString, ",");
138                        while (st.hasMoreTokens()) {
139                                String tok = st.nextToken();
140                                int eqIdx = tok.indexOf('=');
141                                if (eqIdx == -1) {
142                                        throw new IllegalArgumentException(
143                                                        "Expected '=' in attributes CSV string '" + propString + "'");
144                                }
145                                if (eqIdx >= tok.length() - 2) {
146                                        throw new IllegalArgumentException(
147                                                        "At least 2 characters ([]) required in attributes CSV string '" + propString + "'");
148                                }
149                                String name = tok.substring(0, eqIdx);
150                                // Delete first and last characters of value: { and }
151                                int beginIndex = eqIdx + 2;
152                                int endIndex = tok.length() - 1;
153                                String value = tok.substring(beginIndex, endIndex);
154
155                                addStaticAttribute(name, value);
156                        }
157                }
158        }
159
160        /**
161         * Set static attributes for this view from a
162         * {@code java.util.Properties} object.
163         * <p>"Static" attributes are fixed attributes that are specified in
164         * the View instance configuration. "Dynamic" attributes, on the other hand,
165         * are values passed in as part of the model.
166         * <p>This is the most convenient way to set static attributes. Note that
167         * static attributes can be overridden by dynamic attributes, if a value
168         * with the same name is included in the model.
169         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
170         * or a "props" element in XML bean definitions.
171         * @see org.springframework.beans.propertyeditors.PropertiesEditor
172         */
173        public void setAttributes(Properties attributes) {
174                CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes);
175        }
176
177        /**
178         * Set static attributes for this view from a Map. This allows to set
179         * any kind of attribute values, for example bean references.
180         * <p>"Static" attributes are fixed attributes that are specified in
181         * the View instance configuration. "Dynamic" attributes, on the other hand,
182         * are values passed in as part of the model.
183         * <p>Can be populated with a "map" or "props" element in XML bean definitions.
184         * @param attributes a Map with name Strings as keys and attribute objects as values
185         */
186        public void setAttributesMap(@Nullable Map<String, ?> attributes) {
187                if (attributes != null) {
188                        attributes.forEach(this::addStaticAttribute);
189                }
190        }
191
192        /**
193         * Allow Map access to the static attributes of this view,
194         * with the option to add or override specific entries.
195         * <p>Useful for specifying entries directly, for example via
196         * "attributesMap[myKey]". This is particularly useful for
197         * adding or overriding entries in child view definitions.
198         */
199        public Map<String, Object> getAttributesMap() {
200                return this.staticAttributes;
201        }
202
203        /**
204         * Add static data to this view, exposed in each view.
205         * <p>"Static" attributes are fixed attributes that are specified in
206         * the View instance configuration. "Dynamic" attributes, on the other hand,
207         * are values passed in as part of the model.
208         * <p>Must be invoked before any calls to {@code render}.
209         * @param name the name of the attribute to expose
210         * @param value the attribute value to expose
211         * @see #render
212         */
213        public void addStaticAttribute(String name, Object value) {
214                this.staticAttributes.put(name, value);
215        }
216
217        /**
218         * Return the static attributes for this view. Handy for testing.
219         * <p>Returns an unmodifiable Map, as this is not intended for
220         * manipulating the Map but rather just for checking the contents.
221         * @return the static attributes in this view
222         */
223        public Map<String, Object> getStaticAttributes() {
224                return Collections.unmodifiableMap(this.staticAttributes);
225        }
226
227        /**
228         * Specify whether to add path variables to the model or not.
229         * <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable}
230         * annotation. They're are effectively URI template variables with type conversion applied to
231         * them to derive typed Object values. Such values are frequently needed in views for
232         * constructing links to the same and other URLs.
233         * <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
234         * but not attributes already present in the model.
235         * <p>By default this flag is set to {@code true}. Concrete view types can override this.
236         * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise
237         */
238        public void setExposePathVariables(boolean exposePathVariables) {
239                this.exposePathVariables = exposePathVariables;
240        }
241
242        /**
243         * Return whether to add path variables to the model or not.
244         */
245        public boolean isExposePathVariables() {
246                return this.exposePathVariables;
247        }
248
249        /**
250         * Set whether to make all Spring beans in the application context accessible
251         * as request attributes, through lazy checking once an attribute gets accessed.
252         * <p>This will make all such beans accessible in plain {@code ${...}}
253         * expressions in a JSP 2.0 page, as well as in JSTL's {@code c:out}
254         * value expressions.
255         * <p>Default is "false". Switch this flag on to transparently expose all
256         * Spring beans in the request attribute namespace.
257         * <p><b>NOTE:</b> Context beans will override any custom request or session
258         * attributes of the same name that have been manually added. However, model
259         * attributes (as explicitly exposed to this view) of the same name will
260         * always override context beans.
261         * @see #getRequestToExpose
262         */
263        public void setExposeContextBeansAsAttributes(boolean exposeContextBeansAsAttributes) {
264                this.exposeContextBeansAsAttributes = exposeContextBeansAsAttributes;
265        }
266
267        /**
268         * Specify the names of beans in the context which are supposed to be exposed.
269         * If this is non-null, only the specified beans are eligible for exposure as
270         * attributes.
271         * <p>If you'd like to expose all Spring beans in the application context, switch
272         * the {@link #setExposeContextBeansAsAttributes "exposeContextBeansAsAttributes"}
273         * flag on but do not list specific bean names for this property.
274         */
275        public void setExposedContextBeanNames(String... exposedContextBeanNames) {
276                this.exposedContextBeanNames = new HashSet<>(Arrays.asList(exposedContextBeanNames));
277        }
278
279        /**
280         * Set the view's name. Helpful for traceability.
281         * <p>Framework code must call this when constructing views.
282         */
283        @Override
284        public void setBeanName(@Nullable String beanName) {
285                this.beanName = beanName;
286        }
287
288        /**
289         * Return the view's name. Should never be {@code null},
290         * if the view was correctly configured.
291         */
292        @Nullable
293        public String getBeanName() {
294                return this.beanName;
295        }
296
297
298        /**
299         * Prepares the view given the specified model, merging it with static
300         * attributes and a RequestContext attribute, if necessary.
301         * Delegates to renderMergedOutputModel for the actual rendering.
302         * @see #renderMergedOutputModel
303         */
304        @Override
305        public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
306                        HttpServletResponse response) throws Exception {
307
308                if (logger.isDebugEnabled()) {
309                        logger.debug("View " + formatViewName() +
310                                        ", model " + (model != null ? model : Collections.emptyMap()) +
311                                        (this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
312                }
313
314                Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
315                prepareResponse(request, response);
316                renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
317        }
318
319        /**
320         * Creates a combined output Map (never {@code null}) that includes dynamic values and static attributes.
321         * Dynamic values take precedence over static attributes.
322         */
323        protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,
324                        HttpServletRequest request, HttpServletResponse response) {
325
326                @SuppressWarnings("unchecked")
327                Map<String, Object> pathVars = (this.exposePathVariables ?
328                                (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);
329
330                // Consolidate static and dynamic model attributes.
331                int size = this.staticAttributes.size();
332                size += (model != null ? model.size() : 0);
333                size += (pathVars != null ? pathVars.size() : 0);
334
335                Map<String, Object> mergedModel = new LinkedHashMap<>(size);
336                mergedModel.putAll(this.staticAttributes);
337                if (pathVars != null) {
338                        mergedModel.putAll(pathVars);
339                }
340                if (model != null) {
341                        mergedModel.putAll(model);
342                }
343
344                // Expose RequestContext?
345                if (this.requestContextAttribute != null) {
346                        mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
347                }
348
349                return mergedModel;
350        }
351
352        /**
353         * Create a RequestContext to expose under the specified attribute name.
354         * <p>The default implementation creates a standard RequestContext instance for the
355         * given request and model. Can be overridden in subclasses for custom instances.
356         * @param request current HTTP request
357         * @param model combined output Map (never {@code null}),
358         * with dynamic values taking precedence over static attributes
359         * @return the RequestContext instance
360         * @see #setRequestContextAttribute
361         * @see org.springframework.web.servlet.support.RequestContext
362         */
363        protected RequestContext createRequestContext(
364                        HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) {
365
366                return new RequestContext(request, response, getServletContext(), model);
367        }
368
369        /**
370         * Prepare the given response for rendering.
371         * <p>The default implementation applies a workaround for an IE bug
372         * when sending download content via HTTPS.
373         * @param request current HTTP request
374         * @param response current HTTP response
375         */
376        protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
377                if (generatesDownloadContent()) {
378                        response.setHeader("Pragma", "private");
379                        response.setHeader("Cache-Control", "private, must-revalidate");
380                }
381        }
382
383        /**
384         * Return whether this view generates download content
385         * (typically binary content like PDF or Excel files).
386         * <p>The default implementation returns {@code false}. Subclasses are
387         * encouraged to return {@code true} here if they know that they are
388         * generating download content that requires temporary caching on the
389         * client side, typically via the response OutputStream.
390         * @see #prepareResponse
391         * @see javax.servlet.http.HttpServletResponse#getOutputStream()
392         */
393        protected boolean generatesDownloadContent() {
394                return false;
395        }
396
397        /**
398         * Get the request handle to expose to {@link #renderMergedOutputModel}, i.e. to the view.
399         * <p>The default implementation wraps the original request for exposure of Spring beans
400         * as request attributes (if demanded).
401         * @param originalRequest the original servlet request as provided by the engine
402         * @return the wrapped request, or the original request if no wrapping is necessary
403         * @see #setExposeContextBeansAsAttributes
404         * @see #setExposedContextBeanNames
405         * @see org.springframework.web.context.support.ContextExposingHttpServletRequest
406         */
407        protected HttpServletRequest getRequestToExpose(HttpServletRequest originalRequest) {
408                if (this.exposeContextBeansAsAttributes || this.exposedContextBeanNames != null) {
409                        WebApplicationContext wac = getWebApplicationContext();
410                        Assert.state(wac != null, "No WebApplicationContext");
411                        return new ContextExposingHttpServletRequest(originalRequest, wac, this.exposedContextBeanNames);
412                }
413                return originalRequest;
414        }
415
416        /**
417         * Subclasses must implement this method to actually render the view.
418         * <p>The first step will be preparing the request: In the JSP case,
419         * this would mean setting model objects as request attributes.
420         * The second step will be the actual rendering of the view,
421         * for example including the JSP via a RequestDispatcher.
422         * @param model combined output Map (never {@code null}),
423         * with dynamic values taking precedence over static attributes
424         * @param request current HTTP request
425         * @param response current HTTP response
426         * @throws Exception if rendering failed
427         */
428        protected abstract void renderMergedOutputModel(
429                        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
430
431
432        /**
433         * Expose the model objects in the given map as request attributes.
434         * Names will be taken from the model Map.
435         * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}.
436         * @param model a Map of model objects to expose
437         * @param request current HTTP request
438         */
439        protected void exposeModelAsRequestAttributes(Map<String, Object> model,
440                        HttpServletRequest request) throws Exception {
441
442                model.forEach((name, value) -> {
443                        if (value != null) {
444                                request.setAttribute(name, value);
445                        }
446                        else {
447                                request.removeAttribute(name);
448                        }
449                });
450        }
451
452        /**
453         * Create a temporary OutputStream for this view.
454         * <p>This is typically used as IE workaround, for setting the content length header
455         * from the temporary stream before actually writing the content to the HTTP response.
456         */
457        protected ByteArrayOutputStream createTemporaryOutputStream() {
458                return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE);
459        }
460
461        /**
462         * Write the given temporary OutputStream to the HTTP response.
463         * @param response current HTTP response
464         * @param baos the temporary OutputStream to write
465         * @throws IOException if writing/flushing failed
466         */
467        protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException {
468                // Write content type and also length (determined via byte array).
469                response.setContentType(getContentType());
470                response.setContentLength(baos.size());
471
472                // Flush byte array to servlet output stream.
473                ServletOutputStream out = response.getOutputStream();
474                baos.writeTo(out);
475                out.flush();
476        }
477
478        /**
479         * Set the content type of the response to the configured
480         * {@link #setContentType(String) content type} unless the
481         * {@link View#SELECTED_CONTENT_TYPE} request attribute is present and set
482         * to a concrete media type.
483         */
484        protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) {
485                MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE);
486                if (mediaType != null && mediaType.isConcrete()) {
487                        response.setContentType(mediaType.toString());
488                }
489                else {
490                        response.setContentType(getContentType());
491                }
492        }
493
494        @Override
495        public String toString() {
496                return getClass().getName() + ": " + formatViewName();
497        }
498
499        protected String formatViewName() {
500                return (getBeanName() != null ? "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]");
501        }
502
503}