001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.mock.web; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.io.OutputStreamWriter; 023import java.io.PrintWriter; 024import java.io.UnsupportedEncodingException; 025import java.io.Writer; 026import java.nio.charset.Charset; 027import java.text.DateFormat; 028import java.text.ParseException; 029import java.text.SimpleDateFormat; 030import java.time.ZonedDateTime; 031import java.time.format.DateTimeFormatter; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.Date; 036import java.util.List; 037import java.util.Locale; 038import java.util.Map; 039import java.util.TimeZone; 040 041import javax.servlet.ServletOutputStream; 042import javax.servlet.http.Cookie; 043import javax.servlet.http.HttpServletResponse; 044 045import org.springframework.http.HttpHeaders; 046import org.springframework.http.MediaType; 047import org.springframework.lang.Nullable; 048import org.springframework.util.Assert; 049import org.springframework.util.LinkedCaseInsensitiveMap; 050import org.springframework.util.StringUtils; 051import org.springframework.web.util.WebUtils; 052 053/** 054 * Mock implementation of the {@link javax.servlet.http.HttpServletResponse} interface. 055 * 056 * <p>As of Spring Framework 5.0, this set of mocks is designed on a Servlet 4.0 baseline. 057 * 058 * @author Juergen Hoeller 059 * @author Rod Johnson 060 * @author Brian Clozel 061 * @author Vedran Pavic 062 * @author Sebastien Deleuze 063 * @author Sam Brannen 064 * @since 1.0.2 065 */ 066public class MockHttpServletResponse implements HttpServletResponse { 067 068 private static final String CHARSET_PREFIX = "charset="; 069 070 private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; 071 072 private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); 073 074 075 //--------------------------------------------------------------------- 076 // ServletResponse properties 077 //--------------------------------------------------------------------- 078 079 private boolean outputStreamAccessAllowed = true; 080 081 private boolean writerAccessAllowed = true; 082 083 @Nullable 084 private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; 085 086 private boolean charset = false; 087 088 private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024); 089 090 private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content); 091 092 @Nullable 093 private PrintWriter writer; 094 095 private long contentLength = 0; 096 097 @Nullable 098 private String contentType; 099 100 private int bufferSize = 4096; 101 102 private boolean committed; 103 104 private Locale locale = Locale.getDefault(); 105 106 107 //--------------------------------------------------------------------- 108 // HttpServletResponse properties 109 //--------------------------------------------------------------------- 110 111 private final List<Cookie> cookies = new ArrayList<>(); 112 113 private final Map<String, HeaderValueHolder> headers = new LinkedCaseInsensitiveMap<>(); 114 115 private int status = HttpServletResponse.SC_OK; 116 117 @Nullable 118 private String errorMessage; 119 120 @Nullable 121 private String forwardedUrl; 122 123 private final List<String> includedUrls = new ArrayList<>(); 124 125 126 //--------------------------------------------------------------------- 127 // ServletResponse interface 128 //--------------------------------------------------------------------- 129 130 /** 131 * Set whether {@link #getOutputStream()} access is allowed. 132 * <p>Default is {@code true}. 133 */ 134 public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) { 135 this.outputStreamAccessAllowed = outputStreamAccessAllowed; 136 } 137 138 /** 139 * Return whether {@link #getOutputStream()} access is allowed. 140 */ 141 public boolean isOutputStreamAccessAllowed() { 142 return this.outputStreamAccessAllowed; 143 } 144 145 /** 146 * Set whether {@link #getWriter()} access is allowed. 147 * <p>Default is {@code true}. 148 */ 149 public void setWriterAccessAllowed(boolean writerAccessAllowed) { 150 this.writerAccessAllowed = writerAccessAllowed; 151 } 152 153 /** 154 * Return whether {@link #getOutputStream()} access is allowed. 155 */ 156 public boolean isWriterAccessAllowed() { 157 return this.writerAccessAllowed; 158 } 159 160 /** 161 * Return whether the character encoding has been set. 162 * <p>If {@code false}, {@link #getCharacterEncoding()} will return a default encoding value. 163 */ 164 public boolean isCharset() { 165 return this.charset; 166 } 167 168 @Override 169 public void setCharacterEncoding(String characterEncoding) { 170 this.characterEncoding = characterEncoding; 171 this.charset = true; 172 updateContentTypeHeader(); 173 } 174 175 private void updateContentTypeHeader() { 176 if (this.contentType != null) { 177 String value = this.contentType; 178 if (this.charset && !this.contentType.toLowerCase().contains(CHARSET_PREFIX)) { 179 value = value + ';' + CHARSET_PREFIX + this.characterEncoding; 180 } 181 doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true); 182 } 183 } 184 185 @Override 186 @Nullable 187 public String getCharacterEncoding() { 188 return this.characterEncoding; 189 } 190 191 @Override 192 public ServletOutputStream getOutputStream() { 193 Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed"); 194 return this.outputStream; 195 } 196 197 @Override 198 public PrintWriter getWriter() throws UnsupportedEncodingException { 199 Assert.state(this.writerAccessAllowed, "Writer access not allowed"); 200 if (this.writer == null) { 201 Writer targetWriter = (this.characterEncoding != null ? 202 new OutputStreamWriter(this.content, this.characterEncoding) : 203 new OutputStreamWriter(this.content)); 204 this.writer = new ResponsePrintWriter(targetWriter); 205 } 206 return this.writer; 207 } 208 209 public byte[] getContentAsByteArray() { 210 return this.content.toByteArray(); 211 } 212 213 /** 214 * Get the content of the response body as a {@code String}, using the charset 215 * specified for the response by the application, either through 216 * {@link HttpServletResponse} methods or through a charset parameter on the 217 * {@code Content-Type}. 218 * @return the content as a {@code String} 219 * @throws UnsupportedEncodingException if the character encoding is not supported 220 * @see #getContentAsString(Charset) 221 */ 222 public String getContentAsString() throws UnsupportedEncodingException { 223 return (this.characterEncoding != null ? 224 this.content.toString(this.characterEncoding) : this.content.toString()); 225 } 226 227 /** 228 * Get the content of the response body as a {@code String}, using the provided 229 * {@code fallbackCharset} if no charset has been explicitly defined and otherwise 230 * using the charset specified for the response by the application, either 231 * through {@link HttpServletResponse} methods or through a charset parameter on the 232 * {@code Content-Type}. 233 * @return the content as a {@code String} 234 * @throws UnsupportedEncodingException if the character encoding is not supported 235 * @since 5.2 236 * @see #getContentAsString() 237 */ 238 public String getContentAsString(Charset fallbackCharset) throws UnsupportedEncodingException { 239 return (isCharset() && this.characterEncoding != null ? 240 this.content.toString(this.characterEncoding) : 241 this.content.toString(fallbackCharset.name())); 242 } 243 244 @Override 245 public void setContentLength(int contentLength) { 246 this.contentLength = contentLength; 247 doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); 248 } 249 250 public int getContentLength() { 251 return (int) this.contentLength; 252 } 253 254 @Override 255 public void setContentLengthLong(long contentLength) { 256 this.contentLength = contentLength; 257 doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); 258 } 259 260 public long getContentLengthLong() { 261 return this.contentLength; 262 } 263 264 @Override 265 public void setContentType(@Nullable String contentType) { 266 this.contentType = contentType; 267 if (contentType != null) { 268 try { 269 MediaType mediaType = MediaType.parseMediaType(contentType); 270 if (mediaType.getCharset() != null) { 271 this.characterEncoding = mediaType.getCharset().name(); 272 this.charset = true; 273 } 274 } 275 catch (Exception ex) { 276 // Try to get charset value anyway 277 int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); 278 if (charsetIndex != -1) { 279 this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length()); 280 this.charset = true; 281 } 282 } 283 updateContentTypeHeader(); 284 } 285 } 286 287 @Override 288 @Nullable 289 public String getContentType() { 290 return this.contentType; 291 } 292 293 @Override 294 public void setBufferSize(int bufferSize) { 295 this.bufferSize = bufferSize; 296 } 297 298 @Override 299 public int getBufferSize() { 300 return this.bufferSize; 301 } 302 303 @Override 304 public void flushBuffer() { 305 setCommitted(true); 306 } 307 308 @Override 309 public void resetBuffer() { 310 Assert.state(!isCommitted(), "Cannot reset buffer - response is already committed"); 311 this.content.reset(); 312 } 313 314 private void setCommittedIfBufferSizeExceeded() { 315 int bufSize = getBufferSize(); 316 if (bufSize > 0 && this.content.size() > bufSize) { 317 setCommitted(true); 318 } 319 } 320 321 public void setCommitted(boolean committed) { 322 this.committed = committed; 323 } 324 325 @Override 326 public boolean isCommitted() { 327 return this.committed; 328 } 329 330 @Override 331 public void reset() { 332 resetBuffer(); 333 this.characterEncoding = null; 334 this.charset = false; 335 this.contentLength = 0; 336 this.contentType = null; 337 this.locale = Locale.getDefault(); 338 this.cookies.clear(); 339 this.headers.clear(); 340 this.status = HttpServletResponse.SC_OK; 341 this.errorMessage = null; 342 } 343 344 @Override 345 public void setLocale(Locale locale) { 346 this.locale = locale; 347 doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, locale.toLanguageTag(), true); 348 } 349 350 @Override 351 public Locale getLocale() { 352 return this.locale; 353 } 354 355 356 //--------------------------------------------------------------------- 357 // HttpServletResponse interface 358 //--------------------------------------------------------------------- 359 360 @Override 361 public void addCookie(Cookie cookie) { 362 Assert.notNull(cookie, "Cookie must not be null"); 363 this.cookies.add(cookie); 364 doAddHeaderValue(HttpHeaders.SET_COOKIE, getCookieHeader(cookie), false); 365 } 366 367 private String getCookieHeader(Cookie cookie) { 368 StringBuilder buf = new StringBuilder(); 369 buf.append(cookie.getName()).append('=').append(cookie.getValue() == null ? "" : cookie.getValue()); 370 if (StringUtils.hasText(cookie.getPath())) { 371 buf.append("; Path=").append(cookie.getPath()); 372 } 373 if (StringUtils.hasText(cookie.getDomain())) { 374 buf.append("; Domain=").append(cookie.getDomain()); 375 } 376 int maxAge = cookie.getMaxAge(); 377 if (maxAge >= 0) { 378 buf.append("; Max-Age=").append(maxAge); 379 buf.append("; Expires="); 380 ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null); 381 if (expires != null) { 382 buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); 383 } 384 else { 385 HttpHeaders headers = new HttpHeaders(); 386 headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); 387 buf.append(headers.getFirst(HttpHeaders.EXPIRES)); 388 } 389 } 390 391 if (cookie.getSecure()) { 392 buf.append("; Secure"); 393 } 394 if (cookie.isHttpOnly()) { 395 buf.append("; HttpOnly"); 396 } 397 if (cookie instanceof MockCookie) { 398 MockCookie mockCookie = (MockCookie) cookie; 399 if (StringUtils.hasText(mockCookie.getSameSite())) { 400 buf.append("; SameSite=").append(mockCookie.getSameSite()); 401 } 402 } 403 return buf.toString(); 404 } 405 406 public Cookie[] getCookies() { 407 return this.cookies.toArray(new Cookie[0]); 408 } 409 410 @Nullable 411 public Cookie getCookie(String name) { 412 Assert.notNull(name, "Cookie name must not be null"); 413 for (Cookie cookie : this.cookies) { 414 if (name.equals(cookie.getName())) { 415 return cookie; 416 } 417 } 418 return null; 419 } 420 421 @Override 422 public boolean containsHeader(String name) { 423 return (this.headers.get(name) != null); 424 } 425 426 /** 427 * Return the names of all specified headers as a Set of Strings. 428 * <p>As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. 429 * @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none 430 */ 431 @Override 432 public Collection<String> getHeaderNames() { 433 return this.headers.keySet(); 434 } 435 436 /** 437 * Return the primary value for the given header as a String, if any. 438 * Will return the first value in case of multiple values. 439 * <p>As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. 440 * As of Spring 3.1, it returns a stringified value for Servlet 3.0 compatibility. 441 * Consider using {@link #getHeaderValue(String)} for raw Object access. 442 * @param name the name of the header 443 * @return the associated header value, or {@code null} if none 444 */ 445 @Override 446 @Nullable 447 public String getHeader(String name) { 448 HeaderValueHolder header = this.headers.get(name); 449 return (header != null ? header.getStringValue() : null); 450 } 451 452 /** 453 * Return all values for the given header as a List of Strings. 454 * <p>As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. 455 * As of Spring 3.1, it returns a List of stringified values for Servlet 3.0 compatibility. 456 * Consider using {@link #getHeaderValues(String)} for raw Object access. 457 * @param name the name of the header 458 * @return the associated header values, or an empty List if none 459 */ 460 @Override 461 public List<String> getHeaders(String name) { 462 HeaderValueHolder header = this.headers.get(name); 463 if (header != null) { 464 return header.getStringValues(); 465 } 466 else { 467 return Collections.emptyList(); 468 } 469 } 470 471 /** 472 * Return the primary value for the given header, if any. 473 * <p>Will return the first value in case of multiple values. 474 * @param name the name of the header 475 * @return the associated header value, or {@code null} if none 476 */ 477 @Nullable 478 public Object getHeaderValue(String name) { 479 HeaderValueHolder header = this.headers.get(name); 480 return (header != null ? header.getValue() : null); 481 } 482 483 /** 484 * Return all values for the given header as a List of value objects. 485 * @param name the name of the header 486 * @return the associated header values, or an empty List if none 487 */ 488 public List<Object> getHeaderValues(String name) { 489 HeaderValueHolder header = this.headers.get(name); 490 if (header != null) { 491 return header.getValues(); 492 } 493 else { 494 return Collections.emptyList(); 495 } 496 } 497 498 /** 499 * The default implementation returns the given URL String as-is. 500 * <p>Can be overridden in subclasses, appending a session id or the like. 501 */ 502 @Override 503 public String encodeURL(String url) { 504 return url; 505 } 506 507 /** 508 * The default implementation delegates to {@link #encodeURL}, 509 * returning the given URL String as-is. 510 * <p>Can be overridden in subclasses, appending a session id or the like 511 * in a redirect-specific fashion. For general URL encoding rules, 512 * override the common {@link #encodeURL} method instead, applying 513 * to redirect URLs as well as to general URLs. 514 */ 515 @Override 516 public String encodeRedirectURL(String url) { 517 return encodeURL(url); 518 } 519 520 @Override 521 @Deprecated 522 public String encodeUrl(String url) { 523 return encodeURL(url); 524 } 525 526 @Override 527 @Deprecated 528 public String encodeRedirectUrl(String url) { 529 return encodeRedirectURL(url); 530 } 531 532 @Override 533 public void sendError(int status, String errorMessage) throws IOException { 534 Assert.state(!isCommitted(), "Cannot set error status - response is already committed"); 535 this.status = status; 536 this.errorMessage = errorMessage; 537 setCommitted(true); 538 } 539 540 @Override 541 public void sendError(int status) throws IOException { 542 Assert.state(!isCommitted(), "Cannot set error status - response is already committed"); 543 this.status = status; 544 setCommitted(true); 545 } 546 547 @Override 548 public void sendRedirect(String url) throws IOException { 549 Assert.state(!isCommitted(), "Cannot send redirect - response is already committed"); 550 Assert.notNull(url, "Redirect URL must not be null"); 551 setHeader(HttpHeaders.LOCATION, url); 552 setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); 553 setCommitted(true); 554 } 555 556 @Nullable 557 public String getRedirectedUrl() { 558 return getHeader(HttpHeaders.LOCATION); 559 } 560 561 @Override 562 public void setDateHeader(String name, long value) { 563 setHeaderValue(name, formatDate(value)); 564 } 565 566 @Override 567 public void addDateHeader(String name, long value) { 568 addHeaderValue(name, formatDate(value)); 569 } 570 571 public long getDateHeader(String name) { 572 String headerValue = getHeader(name); 573 if (headerValue == null) { 574 return -1; 575 } 576 try { 577 return newDateFormat().parse(getHeader(name)).getTime(); 578 } 579 catch (ParseException ex) { 580 throw new IllegalArgumentException( 581 "Value for header '" + name + "' is not a valid Date: " + headerValue); 582 } 583 } 584 585 private String formatDate(long date) { 586 return newDateFormat().format(new Date(date)); 587 } 588 589 private DateFormat newDateFormat() { 590 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); 591 dateFormat.setTimeZone(GMT); 592 return dateFormat; 593 } 594 595 @Override 596 public void setHeader(String name, String value) { 597 setHeaderValue(name, value); 598 } 599 600 @Override 601 public void addHeader(String name, String value) { 602 addHeaderValue(name, value); 603 } 604 605 @Override 606 public void setIntHeader(String name, int value) { 607 setHeaderValue(name, value); 608 } 609 610 @Override 611 public void addIntHeader(String name, int value) { 612 addHeaderValue(name, value); 613 } 614 615 private void setHeaderValue(String name, Object value) { 616 boolean replaceHeader = true; 617 if (setSpecialHeader(name, value, replaceHeader)) { 618 return; 619 } 620 doAddHeaderValue(name, value, replaceHeader); 621 } 622 623 private void addHeaderValue(String name, Object value) { 624 boolean replaceHeader = false; 625 if (setSpecialHeader(name, value, replaceHeader)) { 626 return; 627 } 628 doAddHeaderValue(name, value, replaceHeader); 629 } 630 631 private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) { 632 if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { 633 setContentType(value.toString()); 634 return true; 635 } 636 else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { 637 setContentLength(value instanceof Number ? ((Number) value).intValue() : 638 Integer.parseInt(value.toString())); 639 return true; 640 } 641 else if (HttpHeaders.CONTENT_LANGUAGE.equalsIgnoreCase(name)) { 642 HttpHeaders headers = new HttpHeaders(); 643 headers.add(HttpHeaders.CONTENT_LANGUAGE, value.toString()); 644 Locale language = headers.getContentLanguage(); 645 setLocale(language != null ? language : Locale.getDefault()); 646 return true; 647 } 648 else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) { 649 MockCookie cookie = MockCookie.parse(value.toString()); 650 if (replaceHeader) { 651 setCookie(cookie); 652 } 653 else { 654 addCookie(cookie); 655 } 656 return true; 657 } 658 else { 659 return false; 660 } 661 } 662 663 private void doAddHeaderValue(String name, Object value, boolean replace) { 664 HeaderValueHolder header = this.headers.get(name); 665 Assert.notNull(value, "Header value must not be null"); 666 if (header == null) { 667 header = new HeaderValueHolder(); 668 this.headers.put(name, header); 669 } 670 if (replace) { 671 header.setValue(value); 672 } 673 else { 674 header.addValue(value); 675 } 676 } 677 678 /** 679 * Set the {@code Set-Cookie} header to the supplied {@link Cookie}, 680 * overwriting any previous cookies. 681 * @param cookie the {@code Cookie} to set 682 * @since 5.1.10 683 * @see #addCookie(Cookie) 684 */ 685 private void setCookie(Cookie cookie) { 686 Assert.notNull(cookie, "Cookie must not be null"); 687 this.cookies.clear(); 688 this.cookies.add(cookie); 689 doAddHeaderValue(HttpHeaders.SET_COOKIE, getCookieHeader(cookie), true); 690 } 691 692 @Override 693 public void setStatus(int status) { 694 if (!this.isCommitted()) { 695 this.status = status; 696 } 697 } 698 699 @Override 700 @Deprecated 701 public void setStatus(int status, String errorMessage) { 702 if (!this.isCommitted()) { 703 this.status = status; 704 this.errorMessage = errorMessage; 705 } 706 } 707 708 @Override 709 public int getStatus() { 710 return this.status; 711 } 712 713 @Nullable 714 public String getErrorMessage() { 715 return this.errorMessage; 716 } 717 718 719 //--------------------------------------------------------------------- 720 // Methods for MockRequestDispatcher 721 //--------------------------------------------------------------------- 722 723 public void setForwardedUrl(@Nullable String forwardedUrl) { 724 this.forwardedUrl = forwardedUrl; 725 } 726 727 @Nullable 728 public String getForwardedUrl() { 729 return this.forwardedUrl; 730 } 731 732 public void setIncludedUrl(@Nullable String includedUrl) { 733 this.includedUrls.clear(); 734 if (includedUrl != null) { 735 this.includedUrls.add(includedUrl); 736 } 737 } 738 739 @Nullable 740 public String getIncludedUrl() { 741 int count = this.includedUrls.size(); 742 Assert.state(count <= 1, 743 () -> "More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls); 744 return (count == 1 ? this.includedUrls.get(0) : null); 745 } 746 747 public void addIncludedUrl(String includedUrl) { 748 Assert.notNull(includedUrl, "Included URL must not be null"); 749 this.includedUrls.add(includedUrl); 750 } 751 752 public List<String> getIncludedUrls() { 753 return this.includedUrls; 754 } 755 756 757 /** 758 * Inner class that adapts the ServletOutputStream to mark the 759 * response as committed once the buffer size is exceeded. 760 */ 761 private class ResponseServletOutputStream extends DelegatingServletOutputStream { 762 763 public ResponseServletOutputStream(OutputStream out) { 764 super(out); 765 } 766 767 @Override 768 public void write(int b) throws IOException { 769 super.write(b); 770 super.flush(); 771 setCommittedIfBufferSizeExceeded(); 772 } 773 774 @Override 775 public void flush() throws IOException { 776 super.flush(); 777 setCommitted(true); 778 } 779 } 780 781 782 /** 783 * Inner class that adapts the PrintWriter to mark the 784 * response as committed once the buffer size is exceeded. 785 */ 786 private class ResponsePrintWriter extends PrintWriter { 787 788 public ResponsePrintWriter(Writer out) { 789 super(out, true); 790 } 791 792 @Override 793 public void write(char[] buf, int off, int len) { 794 super.write(buf, off, len); 795 super.flush(); 796 setCommittedIfBufferSizeExceeded(); 797 } 798 799 @Override 800 public void write(String s, int off, int len) { 801 super.write(s, off, len); 802 super.flush(); 803 setCommittedIfBufferSizeExceeded(); 804 } 805 806 @Override 807 public void write(int c) { 808 super.write(c); 809 super.flush(); 810 setCommittedIfBufferSizeExceeded(); 811 } 812 813 @Override 814 public void flush() { 815 super.flush(); 816 setCommitted(true); 817 } 818 819 @Override 820 public void close() { 821 super.flush(); 822 super.close(); 823 setCommitted(true); 824 } 825 } 826 827}