/* * Copyright 2002-2019 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.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import javax.servlet.ServletOutputStream; import javax.servlet.http.Cookie; import javax.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 javax.servlet.http.HttpServletResponse} interface. * *
As of Spring Framework 5.0, this set of mocks is designed on a Servlet 4.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;
@Nullable
private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
private boolean charset = 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 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;
}
/**
* Return whether the character encoding has been set.
* If {@code false}, {@link #getCharacterEncoding()} will return a default encoding value.
*/
public boolean isCharset() {
return this.charset;
}
@Override
public void setCharacterEncoding(String characterEncoding) {
this.characterEncoding = characterEncoding;
this.charset = true;
updateContentTypeHeader();
}
private void updateContentTypeHeader() {
if (this.contentType != null) {
String value = this.contentType;
if (this.charset && !this.contentType.toLowerCase().contains(CHARSET_PREFIX)) {
value = value + ';' + CHARSET_PREFIX + this.characterEncoding;
}
doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true);
}
}
@Override
@Nullable
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 = (this.characterEncoding != null ?
new OutputStreamWriter(this.content, this.characterEncoding) :
new OutputStreamWriter(this.content));
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}.
* @return the content as a {@code String}
* @throws UnsupportedEncodingException if the character encoding is not supported
* @see #getContentAsString(Charset)
*/
public String getContentAsString() throws UnsupportedEncodingException {
return (this.characterEncoding != null ?
this.content.toString(this.characterEncoding) : this.content.toString());
}
/**
* 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()
*/
public String getContentAsString(Charset fallbackCharset) throws UnsupportedEncodingException {
return (isCharset() && this.characterEncoding != null ?
this.content.toString(this.characterEncoding) :
this.content.toString(fallbackCharset.name()));
}
@Override
public void setContentLength(int contentLength) {
this.contentLength = contentLength;
doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true);
}
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) {
this.characterEncoding = mediaType.getCharset().name();
this.charset = true;
}
}
catch (Exception ex) {
// Try to get charset value anyway
int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
if (charsetIndex != -1) {
this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
this.charset = true;
}
}
updateContentTypeHeader();
}
}
@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 = null;
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(Locale locale) {
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);
}
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();
if (maxAge >= 0) {
buf.append("; Max-Age=").append(maxAge);
buf.append("; Expires=");
if (cookie instanceof MockCookie && ((MockCookie) cookie).getExpires() != null) {
buf.append(((MockCookie) cookie).getExpires().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));
}
}
if (cookie.getSecure()) {
buf.append("; Secure");
}
if (cookie.isHttpOnly()) {
buf.append("; HttpOnly");
}
if (cookie instanceof MockCookie) {
MockCookie mockCookie = (MockCookie) cookie;
if (StringUtils.hasText(mockCookie.getSameSite())) {
buf.append("; SameSite=").append(mockCookie.getSameSite());
}
}
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.get(name) != null);
}
/**
* Return the names of all specified headers as a Set of Strings.
* As of Servlet 3.0, this method is also defined HttpServletResponse.
* @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none
*/
@Override
public Collection As of Servlet 3.0, this method is also defined in HttpServletResponse.
* As of Spring 3.1, it 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
*/
@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.
* As of Servlet 3.0, this method is also defined in HttpServletResponse.
* As of Spring 3.1, it 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
*/
@Override
public List 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