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