org.springframework.web.servlet.view.AbstractView Maven / Gradle / Ivy
/*
* Copyright 2002-2022 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
*
* https://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.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.WebApplicationContext;
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.
*
* 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.
*
*
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;
@Nullable
private String contentType = DEFAULT_CONTENT_TYPE;
@Nullable
private String requestContextAttribute;
private final Map staticAttributes = new LinkedHashMap<>();
private boolean exposePathVariables = true;
private boolean exposeContextBeansAsAttributes = false;
@Nullable
private Set exposedContextBeanNames;
@Nullable
private String beanName;
/**
* Set the content type for this view.
* Default is "text/html;charset=ISO-8859-1".
* 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(@Nullable String contentType) {
this.contentType = contentType;
}
/**
* Return the content type for this view.
*/
@Override
@Nullable
public String getContentType() {
return this.contentType;
}
/**
* Set the name of the RequestContext attribute for this view.
* Default is none.
*/
public void setRequestContextAttribute(@Nullable String requestContextAttribute) {
this.requestContextAttribute = requestContextAttribute;
}
/**
* Return the name of the RequestContext attribute, if any.
*/
@Nullable
public String getRequestContextAttribute() {
return this.requestContextAttribute;
}
/**
* Set static attributes as a CSV string.
* Format is: attname0={value1},attname1={value1}
*
"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(@Nullable 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);
// Delete first and last characters of value: { and }
int beginIndex = eqIdx + 2;
int endIndex = tok.length() - 1;
String value = tok.substring(beginIndex, endIndex);
addStaticAttribute(name, value);
}
}
}
/**
* Set static attributes for this view from a
* {@code java.util.Properties} object.
*
"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.
*
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.
*
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.
*
"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.
*
Can be populated with a "map" or "props" element in XML bean definitions.
* @param attributes a Map with name Strings as keys and attribute objects as values
*/
public void setAttributesMap(@Nullable Map attributes) {
if (attributes != null) {
attributes.forEach(this::addStaticAttribute);
}
}
/**
* Allow {@code Map} access to the static attributes of this view,
* with the option to add or override specific entries.
* Useful for specifying entries directly, for example via
* {@code attributesMap[myKey]}. This is particularly useful for
* adding or overriding entries in child view definitions.
*/
public Map getAttributesMap() {
return this.staticAttributes;
}
/**
* Add static data to this view, exposed in each view.
* "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.
*
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.
*
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 getStaticAttributes() {
return Collections.unmodifiableMap(this.staticAttributes);
}
/**
* Specify whether to add path variables to the model or not.
* Path variables are commonly bound to URI template variables through the {@code @PathVariable}
* annotation. They 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.
*
Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
* but not attributes already present in the model.
*
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.
*
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.
*
Default is "false". Switch this flag on to transparently expose all
* Spring beans in the request attribute namespace.
*
NOTE: 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.
*
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 = Set.of(exposedContextBeanNames);
}
/**
* Set the view's name. Helpful for traceability.
*
Framework code must call this when constructing views.
*/
@Override
public void setBeanName(@Nullable String beanName) {
this.beanName = beanName;
}
/**
* Return the view's name. Should never be {@code null},
* if the view was correctly configured.
*/
@Nullable
public String getBeanName() {
return this.beanName;
}
/**
* 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(@Nullable Map model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("View " + formatViewName() +
", model " + (model != null ? model : Collections.emptyMap()) +
(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
}
Map 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 createMergedOutputModel(@Nullable Map model,
HttpServletRequest request, HttpServletResponse response) {
@SuppressWarnings("unchecked")
Map pathVars = (this.exposePathVariables ?
(Map) 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 mergedModel = CollectionUtils.newLinkedHashMap(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.
* 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 model) {
return new RequestContext(request, response, getServletContext(), model);
}
/**
* Prepare the given response for rendering.
* 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).
*
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 jakarta.servlet.http.HttpServletResponse#getOutputStream()
*/
protected boolean generatesDownloadContent() {
return false;
}
/**
* Get the request handle to expose to {@link #renderMergedOutputModel}, i.e. to the view.
*
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) {
WebApplicationContext wac = getWebApplicationContext();
Assert.state(wac != null, "No WebApplicationContext");
return new ContextExposingHttpServletRequest(originalRequest, wac, this.exposedContextBeanNames);
}
return originalRequest;
}
/**
* Subclasses must implement this method to actually render the view.
*
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 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 jakarta.servlet.RequestDispatcher}.
* @param model a Map of model objects to expose
* @param request current HTTP request
*/
protected void exposeModelAsRequestAttributes(Map model,
HttpServletRequest request) throws Exception {
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
/**
* Create a temporary OutputStream for this view.
* 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() {
return getClass().getName() + ": " + formatViewName();
}
protected String formatViewName() {
return (getBeanName() != null ? "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]");
}
}