1 /*
2  * Copyright 2002-2013 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.springframework.mock.web;
18 
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.OutputStreamWriter;
23 import java.io.PrintWriter;
24 import java.io.UnsupportedEncodingException;
25 import java.io.Writer;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import javax.servlet.ServletOutputStream;
33 import javax.servlet.http.Cookie;
34 import javax.servlet.http.HttpServletResponse;
35 
36 import org.springframework.util.Assert;
37 import org.springframework.util.LinkedCaseInsensitiveMap;
38 import org.springframework.web.util.WebUtils;
39 
40 /**
41  * Mock implementation of the {@link javax.servlet.http.HttpServletResponse} interface.
42  *
43  * <p>Compatible with Servlet 2.5 as well as Servlet 3.0.
44  *
45  * @author Juergen Hoeller
46  * @author Rod Johnson
47  * @since 1.0.2
48  */
49 public class MockHttpServletResponse implements HttpServletResponse {
50 
51 	private static final String CHARSET_PREFIX = "charset=";
52 
53 	private static final String CONTENT_TYPE_HEADER = "Content-Type";
54 
55 	private static final String CONTENT_LENGTH_HEADER = "Content-Length";
56 
57 
58 	//---------------------------------------------------------------------
59 	// ServletResponse properties
60 	//---------------------------------------------------------------------
61 
62 	private boolean outputStreamAccessAllowed = true;
63 
64 	private boolean writerAccessAllowed = true;
65 
66 	private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
67 
68 	private boolean charset = false;
69 
70 	private final ByteArrayOutputStream content = new ByteArrayOutputStream();
71 
72 	private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content);
73 
74 	private PrintWriter writer;
75 
76 	private int contentLength = 0;
77 
78 	private String contentType;
79 
80 	private int bufferSize = 4096;
81 
82 	private boolean committed;
83 
84 	private Locale locale = Locale.getDefault();
85 
86 
87 	//---------------------------------------------------------------------
88 	// HttpServletResponse properties
89 	//---------------------------------------------------------------------
90 
91 	private final List<Cookie> cookies = new ArrayList<Cookie>();
92 
93 	private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<HeaderValueHolder>();
94 
95 	private int status = HttpServletResponse.SC_OK;
96 
97 	private String errorMessage;
98 
99 	private String redirectedUrl;
100 
101 	private String forwardedUrl;
102 
103 	private final List<String> includedUrls = new ArrayList<String>();
104 
105 
106 	//---------------------------------------------------------------------
107 	// ServletResponse interface
108 	//---------------------------------------------------------------------
109 
110 	/**
111 	 * Set whether {@link #getOutputStream()} access is allowed.
112 	 * <p>Default is {@code true}.
113 	 */
setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed)114 	public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) {
115 		this.outputStreamAccessAllowed = outputStreamAccessAllowed;
116 	}
117 
118 	/**
119 	 * Return whether {@link #getOutputStream()} access is allowed.
120 	 */
isOutputStreamAccessAllowed()121 	public boolean isOutputStreamAccessAllowed() {
122 		return this.outputStreamAccessAllowed;
123 	}
124 
125 	/**
126 	 * Set whether {@link #getWriter()} access is allowed.
127 	 * <p>Default is {@code true}.
128 	 */
setWriterAccessAllowed(boolean writerAccessAllowed)129 	public void setWriterAccessAllowed(boolean writerAccessAllowed) {
130 		this.writerAccessAllowed = writerAccessAllowed;
131 	}
132 
133 	/**
134 	 * Return whether {@link #getOutputStream()} access is allowed.
135 	 */
isWriterAccessAllowed()136 	public boolean isWriterAccessAllowed() {
137 		return this.writerAccessAllowed;
138 	}
139 
setCharacterEncoding(String characterEncoding)140 	public void setCharacterEncoding(String characterEncoding) {
141 		this.characterEncoding = characterEncoding;
142 		this.charset = true;
143 		updateContentTypeHeader();
144 	}
145 
updateContentTypeHeader()146 	private void updateContentTypeHeader() {
147 		if (this.contentType != null) {
148 			StringBuilder sb = new StringBuilder(this.contentType);
149 			if (!this.contentType.toLowerCase().contains(CHARSET_PREFIX) && this.charset) {
150 				sb.append(";").append(CHARSET_PREFIX).append(this.characterEncoding);
151 			}
152 			doAddHeaderValue(CONTENT_TYPE_HEADER, sb.toString(), true);
153 		}
154 	}
155 
getCharacterEncoding()156 	public String getCharacterEncoding() {
157 		return this.characterEncoding;
158 	}
159 
getOutputStream()160 	public ServletOutputStream getOutputStream() {
161 		if (!this.outputStreamAccessAllowed) {
162 			throw new IllegalStateException("OutputStream access not allowed");
163 		}
164 		return this.outputStream;
165 	}
166 
getWriter()167 	public PrintWriter getWriter() throws UnsupportedEncodingException {
168 		if (!this.writerAccessAllowed) {
169 			throw new IllegalStateException("Writer access not allowed");
170 		}
171 		if (this.writer == null) {
172 			Writer targetWriter = (this.characterEncoding != null ?
173 					new OutputStreamWriter(this.content, this.characterEncoding) : new OutputStreamWriter(this.content));
174 			this.writer = new ResponsePrintWriter(targetWriter);
175 		}
176 		return this.writer;
177 	}
178 
getContentAsByteArray()179 	public byte[] getContentAsByteArray() {
180 		flushBuffer();
181 		return this.content.toByteArray();
182 	}
183 
getContentAsString()184 	public String getContentAsString() throws UnsupportedEncodingException {
185 		flushBuffer();
186 		return (this.characterEncoding != null) ?
187 				this.content.toString(this.characterEncoding) : this.content.toString();
188 	}
189 
setContentLength(int contentLength)190 	public void setContentLength(int contentLength) {
191 		this.contentLength = contentLength;
192 		doAddHeaderValue(CONTENT_LENGTH_HEADER, contentLength, true);
193 	}
194 
getContentLength()195 	public int getContentLength() {
196 		return this.contentLength;
197 	}
198 
setContentType(String contentType)199 	public void setContentType(String contentType) {
200 		this.contentType = contentType;
201 		if (contentType != null) {
202 			int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX);
203 			if (charsetIndex != -1) {
204 				String encoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length());
205 				this.characterEncoding = encoding;
206 				this.charset = true;
207 			}
208 			updateContentTypeHeader();
209 		}
210 	}
211 
getContentType()212 	public String getContentType() {
213 		return this.contentType;
214 	}
215 
setBufferSize(int bufferSize)216 	public void setBufferSize(int bufferSize) {
217 		this.bufferSize = bufferSize;
218 	}
219 
getBufferSize()220 	public int getBufferSize() {
221 		return this.bufferSize;
222 	}
223 
flushBuffer()224 	public void flushBuffer() {
225 		setCommitted(true);
226 	}
227 
resetBuffer()228 	public void resetBuffer() {
229 		if (isCommitted()) {
230 			throw new IllegalStateException("Cannot reset buffer - response is already committed");
231 		}
232 		this.content.reset();
233 	}
234 
setCommittedIfBufferSizeExceeded()235 	private void setCommittedIfBufferSizeExceeded() {
236 		int bufSize = getBufferSize();
237 		if (bufSize > 0 && this.content.size() > bufSize) {
238 			setCommitted(true);
239 		}
240 	}
241 
setCommitted(boolean committed)242 	public void setCommitted(boolean committed) {
243 		this.committed = committed;
244 	}
245 
isCommitted()246 	public boolean isCommitted() {
247 		return this.committed;
248 	}
249 
reset()250 	public void reset() {
251 		resetBuffer();
252 		this.characterEncoding = null;
253 		this.contentLength = 0;
254 		this.contentType = null;
255 		this.locale = null;
256 		this.cookies.clear();
257 		this.headers.clear();
258 		this.status = HttpServletResponse.SC_OK;
259 		this.errorMessage = null;
260 	}
261 
setLocale(Locale locale)262 	public void setLocale(Locale locale) {
263 		this.locale = locale;
264 	}
265 
getLocale()266 	public Locale getLocale() {
267 		return this.locale;
268 	}
269 
270 
271 	//---------------------------------------------------------------------
272 	// HttpServletResponse interface
273 	//---------------------------------------------------------------------
274 
addCookie(Cookie cookie)275 	public void addCookie(Cookie cookie) {
276 		Assert.notNull(cookie, "Cookie must not be null");
277 		this.cookies.add(cookie);
278 	}
279 
getCookies()280 	public Cookie[] getCookies() {
281 		return this.cookies.toArray(new Cookie[this.cookies.size()]);
282 	}
283 
getCookie(String name)284 	public Cookie getCookie(String name) {
285 		Assert.notNull(name, "Cookie name must not be null");
286 		for (Cookie cookie : this.cookies) {
287 			if (name.equals(cookie.getName())) {
288 				return cookie;
289 			}
290 		}
291 		return null;
292 	}
293 
containsHeader(String name)294 	public boolean containsHeader(String name) {
295 		return (HeaderValueHolder.getByName(this.headers, name) != null);
296 	}
297 
298 	/**
299 	 * Return the names of all specified headers as a Set of Strings.
300 	 * <p>As of Servlet 3.0, this method is also defined HttpServletResponse.
301 	 * @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none
302 	 */
getHeaderNames()303 	public Collection<String> getHeaderNames() {
304 		return this.headers.keySet();
305 	}
306 
307 	/**
308 	 * Return the primary value for the given header as a String, if any.
309 	 * Will return the first value in case of multiple values.
310 	 * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse.
311 	 * As of Spring 3.1, it returns a stringified value for Servlet 3.0 compatibility.
312 	 * Consider using {@link #getHeaderValue(String)} for raw Object access.
313 	 * @param name the name of the header
314 	 * @return the associated header value, or {@code null} if none
315 	 */
getHeader(String name)316 	public String getHeader(String name) {
317 		HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
318 		return (header != null ? header.getStringValue() : null);
319 	}
320 
321 	/**
322 	 * Return all values for the given header as a List of Strings.
323 	 * <p>As of Servlet 3.0, this method is also defined in HttpServletResponse.
324 	 * As of Spring 3.1, it returns a List of stringified values for Servlet 3.0 compatibility.
325 	 * Consider using {@link #getHeaderValues(String)} for raw Object access.
326 	 * @param name the name of the header
327 	 * @return the associated header values, or an empty List if none
328 	 */
getHeaders(String name)329 	public List<String> getHeaders(String name) {
330 		HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
331 		if (header != null) {
332 			return header.getStringValues();
333 		}
334 		else {
335 			return Collections.emptyList();
336 		}
337 	}
338 
339 	/**
340 	 * Return the primary value for the given header, if any.
341 	 * <p>Will return the first value in case of multiple values.
342 	 * @param name the name of the header
343 	 * @return the associated header value, or {@code null} if none
344 	 */
getHeaderValue(String name)345 	public Object getHeaderValue(String name) {
346 		HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
347 		return (header != null ? header.getValue() : null);
348 	}
349 
350 	/**
351 	 * Return all values for the given header as a List of value objects.
352 	 * @param name the name of the header
353 	 * @return the associated header values, or an empty List if none
354 	 */
getHeaderValues(String name)355 	public List<Object> getHeaderValues(String name) {
356 		HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
357 		if (header != null) {
358 			return header.getValues();
359 		}
360 		else {
361 			return Collections.emptyList();
362 		}
363 	}
364 
365 	/**
366 	 * The default implementation returns the given URL String as-is.
367 	 * <p>Can be overridden in subclasses, appending a session id or the like.
368 	 */
encodeURL(String url)369 	public String encodeURL(String url) {
370 		return url;
371 	}
372 
373 	/**
374 	 * The default implementation delegates to {@link #encodeURL},
375 	 * returning the given URL String as-is.
376 	 * <p>Can be overridden in subclasses, appending a session id or the like
377 	 * in a redirect-specific fashion. For general URL encoding rules,
378 	 * override the common {@link #encodeURL} method instead, applying
379 	 * to redirect URLs as well as to general URLs.
380 	 */
encodeRedirectURL(String url)381 	public String encodeRedirectURL(String url) {
382 		return encodeURL(url);
383 	}
384 
encodeUrl(String url)385 	public String encodeUrl(String url) {
386 		return encodeURL(url);
387 	}
388 
encodeRedirectUrl(String url)389 	public String encodeRedirectUrl(String url) {
390 		return encodeRedirectURL(url);
391 	}
392 
sendError(int status, String errorMessage)393 	public void sendError(int status, String errorMessage) throws IOException {
394 		if (isCommitted()) {
395 			throw new IllegalStateException("Cannot set error status - response is already committed");
396 		}
397 		this.status = status;
398 		this.errorMessage = errorMessage;
399 		setCommitted(true);
400 	}
401 
sendError(int status)402 	public void sendError(int status) throws IOException {
403 		if (isCommitted()) {
404 			throw new IllegalStateException("Cannot set error status - response is already committed");
405 		}
406 		this.status = status;
407 		setCommitted(true);
408 	}
409 
sendRedirect(String url)410 	public void sendRedirect(String url) throws IOException {
411 		if (isCommitted()) {
412 			throw new IllegalStateException("Cannot send redirect - response is already committed");
413 		}
414 		Assert.notNull(url, "Redirect URL must not be null");
415 		this.redirectedUrl = url;
416 		setCommitted(true);
417 	}
418 
getRedirectedUrl()419 	public String getRedirectedUrl() {
420 		return this.redirectedUrl;
421 	}
422 
setDateHeader(String name, long value)423 	public void setDateHeader(String name, long value) {
424 		setHeaderValue(name, value);
425 	}
426 
addDateHeader(String name, long value)427 	public void addDateHeader(String name, long value) {
428 		addHeaderValue(name, value);
429 	}
430 
setHeader(String name, String value)431 	public void setHeader(String name, String value) {
432 		setHeaderValue(name, value);
433 	}
434 
addHeader(String name, String value)435 	public void addHeader(String name, String value) {
436 		addHeaderValue(name, value);
437 	}
438 
setIntHeader(String name, int value)439 	public void setIntHeader(String name, int value) {
440 		setHeaderValue(name, value);
441 	}
442 
addIntHeader(String name, int value)443 	public void addIntHeader(String name, int value) {
444 		addHeaderValue(name, value);
445 	}
446 
setHeaderValue(String name, Object value)447 	private void setHeaderValue(String name, Object value) {
448 		if (setSpecialHeader(name, value)) {
449 			return;
450 		}
451 		doAddHeaderValue(name, value, true);
452 	}
453 
addHeaderValue(String name, Object value)454 	private void addHeaderValue(String name, Object value) {
455 		if (setSpecialHeader(name, value)) {
456 			return;
457 		}
458 		doAddHeaderValue(name, value, false);
459 	}
460 
setSpecialHeader(String name, Object value)461 	private boolean setSpecialHeader(String name, Object value) {
462 		if (CONTENT_TYPE_HEADER.equalsIgnoreCase(name)) {
463 			setContentType((String) value);
464 			return true;
465 		}
466 		else if (CONTENT_LENGTH_HEADER.equalsIgnoreCase(name)) {
467 			setContentLength(Integer.parseInt((String) value));
468 			return true;
469 		}
470 		else {
471 			return false;
472 		}
473 	}
474 
doAddHeaderValue(String name, Object value, boolean replace)475 	private void doAddHeaderValue(String name, Object value, boolean replace) {
476 		HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
477 		Assert.notNull(value, "Header value must not be null");
478 		if (header == null) {
479 			header = new HeaderValueHolder();
480 			this.headers.put(name, header);
481 		}
482 		if (replace) {
483 			header.setValue(value);
484 		}
485 		else {
486 			header.addValue(value);
487 		}
488 	}
489 
setStatus(int status)490 	public void setStatus(int status) {
491 		this.status = status;
492 	}
493 
setStatus(int status, String errorMessage)494 	public void setStatus(int status, String errorMessage) {
495 		this.status = status;
496 		this.errorMessage = errorMessage;
497 	}
498 
getStatus()499 	public int getStatus() {
500 		return this.status;
501 	}
502 
getErrorMessage()503 	public String getErrorMessage() {
504 		return this.errorMessage;
505 	}
506 
507 
508 	//---------------------------------------------------------------------
509 	// Methods for MockRequestDispatcher
510 	//---------------------------------------------------------------------
511 
setForwardedUrl(String forwardedUrl)512 	public void setForwardedUrl(String forwardedUrl) {
513 		this.forwardedUrl = forwardedUrl;
514 	}
515 
getForwardedUrl()516 	public String getForwardedUrl() {
517 		return this.forwardedUrl;
518 	}
519 
setIncludedUrl(String includedUrl)520 	public void setIncludedUrl(String includedUrl) {
521 		this.includedUrls.clear();
522 		if (includedUrl != null) {
523 			this.includedUrls.add(includedUrl);
524 		}
525 	}
526 
getIncludedUrl()527 	public String getIncludedUrl() {
528 		int count = this.includedUrls.size();
529 		if (count > 1) {
530 			throw new IllegalStateException(
531 					"More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls);
532 		}
533 		return (count == 1 ? this.includedUrls.get(0) : null);
534 	}
535 
addIncludedUrl(String includedUrl)536 	public void addIncludedUrl(String includedUrl) {
537 		Assert.notNull(includedUrl, "Included URL must not be null");
538 		this.includedUrls.add(includedUrl);
539 	}
540 
getIncludedUrls()541 	public List<String> getIncludedUrls() {
542 		return this.includedUrls;
543 	}
544 
545 
546 	/**
547 	 * Inner class that adapts the ServletOutputStream to mark the
548 	 * response as committed once the buffer size is exceeded.
549 	 */
550 	private class ResponseServletOutputStream extends DelegatingServletOutputStream {
551 
ResponseServletOutputStream(OutputStream out)552 		public ResponseServletOutputStream(OutputStream out) {
553 			super(out);
554 		}
555 
write(int b)556 		public void write(int b) throws IOException {
557 			super.write(b);
558 			super.flush();
559 			setCommittedIfBufferSizeExceeded();
560 		}
561 
flush()562 		public void flush() throws IOException {
563 			super.flush();
564 			setCommitted(true);
565 		}
566 	}
567 
568 
569 	/**
570 	 * Inner class that adapts the PrintWriter to mark the
571 	 * response as committed once the buffer size is exceeded.
572 	 */
573 	private class ResponsePrintWriter extends PrintWriter {
574 
ResponsePrintWriter(Writer out)575 		public ResponsePrintWriter(Writer out) {
576 			super(out, true);
577 		}
578 
write(char buf[], int off, int len)579 		public void write(char buf[], int off, int len) {
580 			super.write(buf, off, len);
581 			super.flush();
582 			setCommittedIfBufferSizeExceeded();
583 		}
584 
write(String s, int off, int len)585 		public void write(String s, int off, int len) {
586 			super.write(s, off, len);
587 			super.flush();
588 			setCommittedIfBufferSizeExceeded();
589 		}
590 
write(int c)591 		public void write(int c) {
592 			super.write(c);
593 			super.flush();
594 			setCommittedIfBufferSizeExceeded();
595 		}
596 
flush()597 		public void flush() {
598 			super.flush();
599 			setCommitted(true);
600 		}
601 	}
602 
603 }
604