org.springframework.mock.web.MockHttpServletResponse Maven / Gradle / Ivy
Show all versions of spring-test Show documentation
/*
* Copyright 2002-2024 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.mock.web;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;
/**
* Mock implementation of the {@link jakarta.servlet.http.HttpServletResponse} interface.
*
* As of Spring 6.0, this set of mocks is designed on a Servlet 6.0 baseline.
*
* @author Juergen Hoeller
* @author Rod Johnson
* @author Brian Clozel
* @author Vedran Pavic
* @author Sebastien Deleuze
* @author Sam Brannen
* @since 1.0.2
*/
public class MockHttpServletResponse implements HttpServletResponse {
private static final String CHARSET_PREFIX = "charset=";
private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
//---------------------------------------------------------------------
// ServletResponse properties
//---------------------------------------------------------------------
private boolean outputStreamAccessAllowed = true;
private boolean writerAccessAllowed = true;
private String defaultCharacterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
private String characterEncoding = this.defaultCharacterEncoding;
/**
* {@code true} if the character encoding has been explicitly set through
* {@link HttpServletResponse} methods or through a {@code charset} parameter
* on the {@code Content-Type}.
*/
private boolean characterEncodingSet = false;
private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024);
private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content);
@Nullable
private PrintWriter writer;
private long contentLength = 0;
@Nullable
private String contentType;
private int bufferSize = 4096;
private boolean committed;
private Locale locale = Locale.getDefault();
//---------------------------------------------------------------------
// HttpServletResponse properties
//---------------------------------------------------------------------
private final List cookies = new ArrayList<>();
private final Map headers = new LinkedCaseInsensitiveMap<>();
private int status = HttpServletResponse.SC_OK;
@Nullable
private String errorMessage;
//---------------------------------------------------------------------
// Properties for MockRequestDispatcher
//---------------------------------------------------------------------
@Nullable
private String forwardedUrl;
private final List includedUrls = new ArrayList<>();
//---------------------------------------------------------------------
// ServletResponse interface
//---------------------------------------------------------------------
/**
* Set whether {@link #getOutputStream()} access is allowed.
* Default is {@code true}.
*/
public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) {
this.outputStreamAccessAllowed = outputStreamAccessAllowed;
}
/**
* Return whether {@link #getOutputStream()} access is allowed.
*/
public boolean isOutputStreamAccessAllowed() {
return this.outputStreamAccessAllowed;
}
/**
* Set whether {@link #getWriter()} access is allowed.
*
Default is {@code true}.
*/
public void setWriterAccessAllowed(boolean writerAccessAllowed) {
this.writerAccessAllowed = writerAccessAllowed;
}
/**
* Return whether {@link #getOutputStream()} access is allowed.
*/
public boolean isWriterAccessAllowed() {
return this.writerAccessAllowed;
}
/**
* Set the default character encoding for the response.
*
If this method is not invoked, {@code ISO-8859-1} will be used as the
* default character encoding.
*
If the {@linkplain #getCharacterEncoding() character encoding} for the
* response has not already been explicitly set via {@link #setCharacterEncoding(String)}
* or {@link #setContentType(String)}, the character encoding for the response
* will be set to the supplied default character encoding.
* @param characterEncoding the default character encoding
* @since 5.3.10
* @see #setCharacterEncoding(String)
* @see #setContentType(String)
*/
public void setDefaultCharacterEncoding(String characterEncoding) {
Assert.notNull(characterEncoding, "'characterEncoding' must not be null");
this.defaultCharacterEncoding = characterEncoding;
if (!this.characterEncodingSet) {
this.characterEncoding = characterEncoding;
}
}
/**
* Determine whether the character encoding has been explicitly set through
* {@link HttpServletResponse} methods or through a {@code charset} parameter
* on the {@code Content-Type}.
*
If {@code false}, {@link #getCharacterEncoding()} will return the
* {@linkplain #setDefaultCharacterEncoding(String) default character encoding}.
*/
public boolean isCharset() {
return this.characterEncodingSet;
}
@Override
public void setCharacterEncoding(@Nullable String characterEncoding) {
setExplicitCharacterEncoding(characterEncoding);
updateContentTypePropertyAndHeader();
}
private void setExplicitCharacterEncoding(@Nullable String characterEncoding) {
if (characterEncoding == null) {
this.characterEncoding = this.defaultCharacterEncoding;
this.characterEncodingSet = false;
if (this.contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(this.contentType);
if (mediaType.getCharset() != null) {
Map parameters = new LinkedHashMap<>(mediaType.getParameters());
parameters.remove("charset");
mediaType = new MediaType(mediaType.getType(), mediaType.getSubtype(), parameters);
this.contentType = mediaType.toString();
}
}
catch (Exception ignored) {
String value = this.contentType;
int charsetIndex = value.toLowerCase().indexOf(CHARSET_PREFIX);
if (charsetIndex != -1) {
value = value.substring(0, charsetIndex).trim();
if (value.endsWith(";")) {
value = value.substring(0, value.length() - 1);
}
this.contentType = value;
}
}
}
}
else {
this.characterEncoding = characterEncoding;
this.characterEncodingSet = true;
}
}
private void updateContentTypePropertyAndHeader() {
if (this.contentType != null) {
String value = this.contentType;
if (this.characterEncodingSet && !value.toLowerCase().contains(CHARSET_PREFIX)) {
value += ';' + CHARSET_PREFIX + getCharacterEncoding();
this.contentType = value;
}
doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true);
}
}
@Override
public String getCharacterEncoding() {
return this.characterEncoding;
}
@Override
public ServletOutputStream getOutputStream() {
Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed");
return this.outputStream;
}
@Override
public PrintWriter getWriter() throws UnsupportedEncodingException {
Assert.state(this.writerAccessAllowed, "Writer access not allowed");
if (this.writer == null) {
Writer targetWriter = new OutputStreamWriter(this.content, getCharacterEncoding());
this.writer = new ResponsePrintWriter(targetWriter);
}
return this.writer;
}
public byte[] getContentAsByteArray() {
return this.content.toByteArray();
}
/**
* Get the content of the response body as a {@code String}, using the charset
* specified for the response by the application, either through
* {@link HttpServletResponse} methods or through a charset parameter on the
* {@code Content-Type}. If no charset has been explicitly defined, the
* {@linkplain #setDefaultCharacterEncoding(String) default character encoding}
* will be used.
* @return the content as a {@code String}
* @throws UnsupportedEncodingException if the character encoding is not supported
* @see #getContentAsString(Charset)
* @see #setCharacterEncoding(String)
* @see #setContentType(String)
*/
public String getContentAsString() throws UnsupportedEncodingException {
return this.content.toString(getCharacterEncoding());
}
/**
* Get the content of the response body as a {@code String}, using the provided
* {@code fallbackCharset} if no charset has been explicitly defined and otherwise
* using the charset specified for the response by the application, either
* through {@link HttpServletResponse} methods or through a charset parameter on the
* {@code Content-Type}.
* @return the content as a {@code String}
* @throws UnsupportedEncodingException if the character encoding is not supported
* @since 5.2
* @see #getContentAsString()
* @see #setCharacterEncoding(String)
* @see #setContentType(String)
*/
public String getContentAsString(Charset fallbackCharset) throws UnsupportedEncodingException {
if (this.characterEncodingSet) {
return this.content.toString(getCharacterEncoding());
}
return this.content.toString(fallbackCharset);
}
@Override
public void setContentLength(int contentLength) {
this.contentLength = contentLength;
doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true);
}
/**
* Get the length of the content body from the HTTP Content-Length header.
* @return the value of the Content-Length header
* @see #setContentLength(int)
*/
public int getContentLength() {
return (int) this.contentLength;
}
@Override
public void setContentLengthLong(long contentLength) {
this.contentLength = contentLength;
doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true);
}
public long getContentLengthLong() {
return this.contentLength;
}
@Override
public void setContentType(@Nullable String contentType) {
this.contentType = contentType;
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
if (mediaType.getCharset() != null) {
setExplicitCharacterEncoding(mediaType.getCharset().name());
}
}
catch (Exception ex) {
// Try to get charset value anyway
int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
if (charsetIndex != -1) {
setExplicitCharacterEncoding(contentType.substring(charsetIndex + CHARSET_PREFIX.length()));
}
}
updateContentTypePropertyAndHeader();
}
}
@Override
@Nullable
public String getContentType() {
return this.contentType;
}
@Override
public void setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
}
@Override
public int getBufferSize() {
return this.bufferSize;
}
@Override
public void flushBuffer() {
setCommitted(true);
}
@Override
public void resetBuffer() {
Assert.state(!isCommitted(), "Cannot reset buffer - response is already committed");
this.content.reset();
}
private void setCommittedIfBufferSizeExceeded() {
int bufSize = getBufferSize();
if (bufSize > 0 && this.content.size() > bufSize) {
setCommitted(true);
}
}
public void setCommitted(boolean committed) {
this.committed = committed;
}
@Override
public boolean isCommitted() {
return this.committed;
}
@Override
public void reset() {
resetBuffer();
this.characterEncoding = this.defaultCharacterEncoding;
this.characterEncodingSet = false;
this.contentLength = 0;
this.contentType = null;
this.locale = Locale.getDefault();
this.cookies.clear();
this.headers.clear();
this.status = HttpServletResponse.SC_OK;
this.errorMessage = null;
}
@Override
public void setLocale(@Nullable Locale locale) {
// Although the Javadoc for jakarta.servlet.ServletResponse.setLocale(Locale) does not
// state how a null value for the supplied Locale should be handled, both Tomcat and
// Jetty simply ignore a null value. So we do the same here.
if (locale == null) {
return;
}
this.locale = locale;
doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, locale.toLanguageTag(), true);
}
@Override
public Locale getLocale() {
return this.locale;
}
//---------------------------------------------------------------------
// HttpServletResponse interface
//---------------------------------------------------------------------
@Override
public void addCookie(Cookie cookie) {
Assert.notNull(cookie, "Cookie must not be null");
this.cookies.add(cookie);
doAddHeaderValue(HttpHeaders.SET_COOKIE, getCookieHeader(cookie), false);
}
@SuppressWarnings("removal")
private String getCookieHeader(Cookie cookie) {
StringBuilder buf = new StringBuilder();
buf.append(cookie.getName()).append('=').append(cookie.getValue() == null ? "" : cookie.getValue());
if (StringUtils.hasText(cookie.getPath())) {
buf.append("; Path=").append(cookie.getPath());
}
if (StringUtils.hasText(cookie.getDomain())) {
buf.append("; Domain=").append(cookie.getDomain());
}
int maxAge = cookie.getMaxAge();
ZonedDateTime expires = (cookie instanceof MockCookie mockCookie ? mockCookie.getExpires() : null);
if (maxAge >= 0) {
buf.append("; Max-Age=").append(maxAge);
buf.append("; Expires=");
if (expires != null) {
buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
else {
HttpHeaders headers = new HttpHeaders();
headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0);
buf.append(headers.getFirst(HttpHeaders.EXPIRES));
}
}
else if (expires != null) {
buf.append("; Expires=");
buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
if (cookie.getSecure()) {
buf.append("; Secure");
}
if (cookie.isHttpOnly()) {
buf.append("; HttpOnly");
}
if (cookie instanceof MockCookie mockCookie) {
if (StringUtils.hasText(mockCookie.getSameSite())) {
buf.append("; SameSite=").append(mockCookie.getSameSite());
}
}
if (StringUtils.hasText(cookie.getComment())) {
buf.append("; Comment=").append(cookie.getComment());
}
return buf.toString();
}
public Cookie[] getCookies() {
return this.cookies.toArray(new Cookie[0]);
}
@Nullable
public Cookie getCookie(String name) {
Assert.notNull(name, "Cookie name must not be null");
for (Cookie cookie : this.cookies) {
if (name.equals(cookie.getName())) {
return cookie;
}
}
return null;
}
@Override
public boolean containsHeader(String name) {
return this.headers.containsKey(name);
}
/**
* Return the names of all specified headers as a Set of Strings.
* As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}.
* @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none
*/
@Override
public Collection getHeaderNames() {
return this.headers.keySet();
}
/**
* Return the primary value for the given header as a String, if any.
* Will return the first value in case of multiple values.
*
Returns a stringified value for Servlet 3.0 compatibility. Consider
* using {@link #getHeaderValue(String)} for raw Object access.
* @param name the name of the header
* @return the associated header value, or {@code null} if none
* @see HttpServletResponse#getHeader(String)
*/
@Override
@Nullable
public String getHeader(String name) {
HeaderValueHolder header = this.headers.get(name);
return (header != null ? header.getStringValue() : null);
}
/**
* Return all values for the given header as a List of Strings.
*
Returns a List of stringified values for Servlet 3.0 compatibility.
* Consider using {@link #getHeaderValues(String)} for raw Object access.
* @param name the name of the header
* @return the associated header values, or an empty List if none
* @see HttpServletResponse#getHeaders(String)
*/
@Override
public List getHeaders(String name) {
HeaderValueHolder header = this.headers.get(name);
if (header != null) {
return header.getStringValues();
}
else {
return Collections.emptyList();
}
}
/**
* Return the primary value for the given header, if any.
* Will return the first value in case of multiple values.
* @param name the name of the header
* @return the associated header value, or {@code null} if none
*/
@Nullable
public Object getHeaderValue(String name) {
HeaderValueHolder header = this.headers.get(name);
return (header != null ? header.getValue() : null);
}
/**
* Return all values for the given header as a List of value objects.
* @param name the name of the header
* @return the associated header values, or an empty List if none
*/
public List