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}