/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.servlet.view; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.BeanNameAware; import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; import org.springframework.web.context.support.ContextExposingHttpServletRequest; import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.servlet.View; import org.springframework.web.servlet.support.RequestContext; /** * Abstract base class for {@link org.springframework.web.servlet.View} * implementations. Subclasses should be JavaBeans, to allow for * convenient configuration as Spring-managed bean instances. * * <p>Provides support for static attributes, to be made available to the view, * with a variety of ways to specify them. Static attributes will be merged * with the given dynamic attributes (the model that the controller returned) * for each render operation. * * <p>Extends {@link WebApplicationObjectSupport}, which will be helpful to * some views. Subclasses just need to implement the actual rendering. * * @author Rod Johnson * @author Juergen Hoeller * @see #setAttributes * @see #setAttributesMap * @see #renderMergedOutputModel */ public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware { /** Default content type. Overridable as bean property. */ public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1"; /** Initial size for the temporary output byte array (if any) */ private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096; private String beanName; private String contentType = DEFAULT_CONTENT_TYPE; private String requestContextAttribute; private final Map<String, Object> staticAttributes = new LinkedHashMap<>(); private boolean exposePathVariables = true; private boolean exposeContextBeansAsAttributes = false; private Set<String> exposedContextBeanNames; /** * Set the view's name. Helpful for traceability. * <p>Framework code must call this when constructing views. */ @Override public void setBeanName(String beanName) { this.beanName = beanName; } /** * Return the view's name. Should never be {@code null}, * if the view was correctly configured. */ public String getBeanName() { return this.beanName; } /** * Set the content type for this view. * Default is "text/html;charset=ISO-8859-1". * <p>May be ignored by subclasses if the view itself is assumed * to set the content type, e.g. in case of JSPs. */ public void setContentType(String contentType) { this.contentType = contentType; } /** * Return the content type for this view. */ @Override public String getContentType() { return this.contentType; } /** * Set the name of the RequestContext attribute for this view. * Default is none. */ public void setRequestContextAttribute(String requestContextAttribute) { this.requestContextAttribute = requestContextAttribute; } /** * Return the name of the RequestContext attribute, if any. */ public String getRequestContextAttribute() { return this.requestContextAttribute; } /** * Set static attributes as a CSV string. * Format is: attname0={value1},attname1={value1} * <p>"Static" attributes are fixed attributes that are specified in * the View instance configuration. "Dynamic" attributes, on the other hand, * are values passed in as part of the model. */ public void setAttributesCSV(String propString) throws IllegalArgumentException { if (propString != null) { StringTokenizer st = new StringTokenizer(propString, ","); while (st.hasMoreTokens()) { String tok = st.nextToken(); int eqIdx = tok.indexOf("="); if (eqIdx == -1) { throw new IllegalArgumentException("Expected = in attributes CSV string '" + propString + "'"); } if (eqIdx >= tok.length() - 2) { throw new IllegalArgumentException( "At least 2 characters ([]) required in attributes CSV string '" + propString + "'"); } String name = tok.substring(0, eqIdx); String value = tok.substring(eqIdx + 1); // Delete first and last characters of value: { and } value = value.substring(1); value = value.substring(0, value.length() - 1); addStaticAttribute(name, value); } } } /** * Set static attributes for this view from a * {@code java.util.Properties} object. * <p>"Static" attributes are fixed attributes that are specified in * the View instance configuration. "Dynamic" attributes, on the other hand, * are values passed in as part of the model. * <p>This is the most convenient way to set static attributes. Note that * static attributes can be overridden by dynamic attributes, if a value * with the same name is included in the model. * <p>Can be populated with a String "value" (parsed via PropertiesEditor) * or a "props" element in XML bean definitions. * @see org.springframework.beans.propertyeditors.PropertiesEditor */ public void setAttributes(Properties attributes) { CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes); } /** * Set static attributes for this view from a Map. This allows to set * any kind of attribute values, for example bean references. * <p>"Static" attributes are fixed attributes that are specified in * the View instance configuration. "Dynamic" attributes, on the other hand, * are values passed in as part of the model. * <p>Can be populated with a "map" or "props" element in XML bean definitions. * @param attributes Map with name Strings as keys and attribute objects as values */ public void setAttributesMap(Map<String, ?> attributes) { if (attributes != null) { for (Map.Entry<String, ?> entry : attributes.entrySet()) { addStaticAttribute(entry.getKey(), entry.getValue()); } } } /** * Allow Map access to the static attributes of this view, * with the option to add or override specific entries. * <p>Useful for specifying entries directly, for example via * "attributesMap[myKey]". This is particularly useful for * adding or overriding entries in child view definitions. */ public Map<String, Object> getAttributesMap() { return this.staticAttributes; } /** * Add static data to this view, exposed in each view. * <p>"Static" attributes are fixed attributes that are specified in * the View instance configuration. "Dynamic" attributes, on the other hand, * are values passed in as part of the model. * <p>Must be invoked before any calls to {@code render}. * @param name the name of the attribute to expose * @param value the attribute value to expose * @see #render */ public void addStaticAttribute(String name, Object value) { this.staticAttributes.put(name, value); } /** * Return the static attributes for this view. Handy for testing. * <p>Returns an unmodifiable Map, as this is not intended for * manipulating the Map but rather just for checking the contents. * @return the static attributes in this view */ public Map<String, Object> getStaticAttributes() { return Collections.unmodifiableMap(this.staticAttributes); } /** * Specify whether to add path variables to the model or not. * <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable} * annotation. They're are effectively URI template variables with type conversion applied to * them to derive typed Object values. Such values are frequently needed in views for * constructing links to the same and other URLs. * <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)}) * but not attributes already present in the model. * <p>By default this flag is set to {@code true}. Concrete view types can override this. * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise */ public void setExposePathVariables(boolean exposePathVariables) { this.exposePathVariables = exposePathVariables; } /** * Return whether to add path variables to the model or not. */ public boolean isExposePathVariables() { return this.exposePathVariables; } /** * Set whether to make all Spring beans in the application context accessible * as request attributes, through lazy checking once an attribute gets accessed. * <p>This will make all such beans accessible in plain {@code ${...}} * expressions in a JSP 2.0 page, as well as in JSTL's {@code c:out} * value expressions. * <p>Default is "false". Switch this flag on to transparently expose all * Spring beans in the request attribute namespace. * <p><b>NOTE:</b> Context beans will override any custom request or session * attributes of the same name that have been manually added. However, model * attributes (as explicitly exposed to this view) of the same name will * always override context beans. * @see #getRequestToExpose */ public void setExposeContextBeansAsAttributes(boolean exposeContextBeansAsAttributes) { this.exposeContextBeansAsAttributes = exposeContextBeansAsAttributes; } /** * Specify the names of beans in the context which are supposed to be exposed. * If this is non-null, only the specified beans are eligible for exposure as * attributes. * <p>If you'd like to expose all Spring beans in the application context, switch * the {@link #setExposeContextBeansAsAttributes "exposeContextBeansAsAttributes"} * flag on but do not list specific bean names for this property. */ public void setExposedContextBeanNames(String... exposedContextBeanNames) { this.exposedContextBeanNames = new HashSet<>(Arrays.asList(exposedContextBeanNames)); } /** * Prepares the view given the specified model, merging it with static * attributes and a RequestContext attribute, if necessary. * Delegates to renderMergedOutputModel for the actual rendering. * @see #renderMergedOutputModel */ @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (logger.isTraceEnabled()) { logger.trace("Rendering view with name '" + this.beanName + "' with model " + model + " and static attributes " + this.staticAttributes); } Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); } /** * Creates a combined output Map (never {@code null}) that includes dynamic values and static attributes. * Dynamic values take precedence over static attributes. */ protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { @SuppressWarnings("unchecked") Map<String, Object> pathVars = (this.exposePathVariables ? (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null); // Consolidate static and dynamic model attributes. int size = this.staticAttributes.size(); size += (model != null ? model.size() : 0); size += (pathVars != null ? pathVars.size() : 0); Map<String, Object> mergedModel = new LinkedHashMap<>(size); mergedModel.putAll(this.staticAttributes); if (pathVars != null) { mergedModel.putAll(pathVars); } if (model != null) { mergedModel.putAll(model); } // Expose RequestContext? if (this.requestContextAttribute != null) { mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); } return mergedModel; } /** * Create a RequestContext to expose under the specified attribute name. * <p>The default implementation creates a standard RequestContext instance for the * given request and model. Can be overridden in subclasses for custom instances. * @param request current HTTP request * @param model combined output Map (never {@code null}), * with dynamic values taking precedence over static attributes * @return the RequestContext instance * @see #setRequestContextAttribute * @see org.springframework.web.servlet.support.RequestContext */ protected RequestContext createRequestContext( HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) { return new RequestContext(request, response, getServletContext(), model); } /** * Prepare the given response for rendering. * <p>The default implementation applies a workaround for an IE bug * when sending download content via HTTPS. * @param request current HTTP request * @param response current HTTP response */ protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { if (generatesDownloadContent()) { response.setHeader("Pragma", "private"); response.setHeader("Cache-Control", "private, must-revalidate"); } } /** * Return whether this view generates download content * (typically binary content like PDF or Excel files). * <p>The default implementation returns {@code false}. Subclasses are * encouraged to return {@code true} here if they know that they are * generating download content that requires temporary caching on the * client side, typically via the response OutputStream. * @see #prepareResponse * @see javax.servlet.http.HttpServletResponse#getOutputStream() */ protected boolean generatesDownloadContent() { return false; } /** * Get the request handle to expose to {@link #renderMergedOutputModel}, i.e. to the view. * <p>The default implementation wraps the original request for exposure of Spring beans * as request attributes (if demanded). * @param originalRequest the original servlet request as provided by the engine * @return the wrapped request, or the original request if no wrapping is necessary * @see #setExposeContextBeansAsAttributes * @see #setExposedContextBeanNames * @see org.springframework.web.context.support.ContextExposingHttpServletRequest */ protected HttpServletRequest getRequestToExpose(HttpServletRequest originalRequest) { if (this.exposeContextBeansAsAttributes || this.exposedContextBeanNames != null) { return new ContextExposingHttpServletRequest( originalRequest, getWebApplicationContext(), this.exposedContextBeanNames); } return originalRequest; } /** * Subclasses must implement this method to actually render the view. * <p>The first step will be preparing the request: In the JSP case, * this would mean setting model objects as request attributes. * The second step will be the actual rendering of the view, * for example including the JSP via a RequestDispatcher. * @param model combined output Map (never {@code null}), * with dynamic values taking precedence over static attributes * @param request current HTTP request * @param response current HTTP response * @throws Exception if rendering failed */ protected abstract void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception; /** * Expose the model objects in the given map as request attributes. * Names will be taken from the model Map. * This method is suitable for all resources reachable by {@link javax.servlet.RequestDispatcher}. * @param model Map of model objects to expose * @param request current HTTP request */ protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception { for (Map.Entry<String, Object> entry : model.entrySet()) { String modelName = entry.getKey(); Object modelValue = entry.getValue(); if (modelValue != null) { request.setAttribute(modelName, modelValue); if (logger.isDebugEnabled()) { logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass().getName() + "] to request in view with name '" + getBeanName() + "'"); } } else { request.removeAttribute(modelName); if (logger.isDebugEnabled()) { logger.debug("Removed model object '" + modelName + "' from request in view with name '" + getBeanName() + "'"); } } } } /** * Create a temporary OutputStream for this view. * <p>This is typically used as IE workaround, for setting the content length header * from the temporary stream before actually writing the content to the HTTP response. */ protected ByteArrayOutputStream createTemporaryOutputStream() { return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); } /** * Write the given temporary OutputStream to the HTTP response. * @param response current HTTP response * @param baos the temporary OutputStream to write * @throws IOException if writing/flushing failed */ protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException { // Write content type and also length (determined via byte array). response.setContentType(getContentType()); response.setContentLength(baos.size()); // Flush byte array to servlet output stream. ServletOutputStream out = response.getOutputStream(); baos.writeTo(out); out.flush(); } /** * Set the content type of the response to the configured * {@link #setContentType(String) content type} unless the * {@link View#SELECTED_CONTENT_TYPE} request attribute is present and set * to a concrete media type. */ protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) { MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE); if (mediaType != null && mediaType.isConcrete()) { response.setContentType(mediaType.toString()); } else { response.setContentType(getContentType()); } } @Override public String toString() { StringBuilder sb = new StringBuilder(getClass().getName()); if (getBeanName() != null) { sb.append(": name '").append(getBeanName()).append("'"); } else { sb.append(": unnamed"); } return sb.toString(); } }