Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / ResponseRewriterFilter.java
blob52d0463d28e8fd71c08b0a2b6805f0bb1b805f6b
1 // Copyright 2012 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import com.google.appengine.api.log.dev.LocalLogService;
6 import com.google.common.base.Optional;
7 import com.google.common.base.Preconditions;
8 import com.google.common.net.HttpHeaders;
10 import java.io.ByteArrayOutputStream;
11 import java.io.IOException;
12 import java.io.OutputStream;
13 import java.io.OutputStreamWriter;
14 import java.io.PrintWriter;
15 import java.io.UnsupportedEncodingException;
16 import java.text.ParseException;
17 import java.text.SimpleDateFormat;
18 import java.util.Date;
19 import java.util.Enumeration;
20 import java.util.Locale;
21 import java.util.NoSuchElementException;
22 import java.util.TimeZone;
23 import java.util.Vector;
25 import javax.servlet.Filter;
26 import javax.servlet.FilterChain;
27 import javax.servlet.FilterConfig;
28 import javax.servlet.ServletException;
29 import javax.servlet.ServletOutputStream;
30 import javax.servlet.ServletRequest;
31 import javax.servlet.ServletResponse;
32 import javax.servlet.http.Cookie;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletRequestWrapper;
35 import javax.servlet.http.HttpServletResponse;
36 import javax.servlet.http.HttpServletResponseWrapper;
38 /**
39 * A filter that rewrites the response headers and body from the user's
40 * application.
42 * <p>This sanitises the headers to ensure that they are sensible and the user
43 * is not setting sensitive headers, such as Content-Length, incorrectly. It
44 * also deletes the body if the response status code indicates a non-body
45 * status.
47 * <p>This also strips out some request headers before passing the request to
48 * the application.
51 public class ResponseRewriterFilter implements Filter {
52 /**
53 * A mock timestamp to use as the response completion time, for testing.
55 * <p>Long.MIN_VALUE indicates that this should not be used, and instead, the
56 * current time should be taken.
58 private final long emulatedResponseTime;
59 private LocalLogService logService;
61 private static final String BLOB_KEY_HEADER = "X-AppEngine-BlobKey";
63 /** The value of the "Server" header output by the development server. */
64 private static final String DEVELOPMENT_SERVER = "Development/1.0";
66 /** These statuses must not include a response body (RFC 2616). */
67 private static final int[] NO_BODY_RESPONSE_STATUSES = {
68 HttpServletResponse.SC_CONTINUE,
69 HttpServletResponse.SC_SWITCHING_PROTOCOLS,
70 HttpServletResponse.SC_NO_CONTENT,
71 HttpServletResponse.SC_NOT_MODIFIED,
74 public ResponseRewriterFilter() {
75 super();
76 emulatedResponseTime = Long.MIN_VALUE;
79 /**
80 * Creates a ResponseRewriterFilter for testing purposes, which mocks the
81 * current time.
83 * @param mockTimestamp Indicates that the current time will be emulated with
84 * this timestamp.
86 public ResponseRewriterFilter(long mockTimestamp) {
87 super();
88 emulatedResponseTime = mockTimestamp;
91 @Override
92 public void init(FilterConfig filterConfig) {
93 Object apiProxyDelegate = filterConfig.getServletContext().getAttribute(
94 "com.google.appengine.devappserver.ApiProxyLocal");
95 if (apiProxyDelegate instanceof ApiProxyLocal) {
96 ApiProxyLocal apiProxyLocal = (ApiProxyLocal) apiProxyDelegate;
97 logService = (LocalLogService) apiProxyLocal.getService(LocalLogService.PACKAGE);
102 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
103 * javax.servlet.ServletResponse,
104 * javax.servlet.FilterChain)
106 @Override
107 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
108 throws IOException, ServletException {
109 HttpServletRequest httprequest;
110 HttpServletResponse httpresponse;
111 try {
112 httprequest = (HttpServletRequest) request;
113 httpresponse = (HttpServletResponse) response;
114 } catch (ClassCastException e) {
115 throw new ServletException(e);
118 RequestWrapper wrappedRequest = new RequestWrapper(httprequest);
119 ResponseWrapper wrappedResponse = new ResponseWrapper(httpresponse);
121 chain.doFilter(wrappedRequest, wrappedResponse);
123 Preconditions.checkState(!response.isCommitted(), "Response has already been committed");
125 long responseTime;
126 if (emulatedResponseTime == Long.MIN_VALUE) {
127 responseTime = System.currentTimeMillis();
128 } else {
129 responseTime = emulatedResponseTime;
132 ignoreHeadersRewriter(wrappedRequest, wrappedResponse, responseTime);
133 serverDateRewriter(wrappedRequest, wrappedResponse, responseTime);
134 cacheRewriter(wrappedRequest, wrappedResponse, responseTime);
135 blobServeRewriter(wrappedRequest, wrappedResponse, responseTime);
136 contentLengthRewriter(wrappedRequest, wrappedResponse, responseTime);
138 wrappedResponse.reallyCommit();
141 private static final String[] IGNORE_REQUEST_HEADERS = {
142 HttpHeaders.ACCEPT_ENCODING,
143 HttpHeaders.CONNECTION,
144 "Keep-Alive",
145 HttpHeaders.PROXY_AUTHORIZATION,
146 HttpHeaders.TE,
147 HttpHeaders.TRAILER,
148 HttpHeaders.TRANSFER_ENCODING,
151 private static final String[] IGNORE_RESPONSE_HEADERS = {
152 HttpHeaders.CONNECTION,
153 HttpHeaders.CONTENT_ENCODING,
154 HttpHeaders.DATE,
155 "Keep-Alive",
156 HttpHeaders.PROXY_AUTHENTICATE,
157 HttpHeaders.SERVER,
158 HttpHeaders.TRAILER,
159 HttpHeaders.TRANSFER_ENCODING,
160 HttpHeaders.UPGRADE,
164 * Removes specific response headers.
166 * <p>Certain response headers cannot be modified by an Application. This
167 * rewriter simply removes those headers.
169 * @param request A request object, which is not modified.
170 * @param response A response object, which may be modified.
171 * @param responseTime The timestamp indicating when the response completed.
173 private void ignoreHeadersRewriter(HttpServletRequest request, ResponseWrapper response,
174 long responseTime) {
175 for (String h : IGNORE_RESPONSE_HEADERS) {
176 if (response.containsHeader(h)) {
177 response.reallySetHeader(h, null);
183 * Sets the Server and Date response headers to their correct value.
185 * @param request A request object, which is not modified.
186 * @param response A response object, which may be modified.
187 * @param responseTime The timestamp indicating when the response completed.
189 private void serverDateRewriter(HttpServletRequest request, ResponseWrapper response,
190 long responseTime) {
191 response.reallySetHeader(HttpHeaders.SERVER, DEVELOPMENT_SERVER);
192 response.reallySetDateHeader(HttpHeaders.DATE, responseTime);
196 * Determines whether the response may have a body, based on the status code.
198 * @param status The response status code.
199 * @return true if the response may have a body.
201 private static boolean responseMayHaveBody(int status) {
202 for (int s : NO_BODY_RESPONSE_STATUSES) {
203 if (status == s) {
204 return false;
207 return true;
211 * Sets the default Cache-Control and Expires headers.
213 * These are only set if the response status allows a body, and only if the
214 * headers have not been explicitly set by the application.
216 * @param request A request object, which is not modified.
217 * @param response A response object, which may be modified.
218 * @param responseTime The timestamp indicating when the response completed.
220 private void cacheRewriter(HttpServletRequest request, ResponseWrapper response,
221 long responseTime) {
222 if (!responseMayHaveBody(response.getStatus())) {
223 return;
226 if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
227 response.reallySetHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
228 if (!response.containsHeader(HttpHeaders.EXPIRES)) {
229 response.reallySetHeader(HttpHeaders.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT");
233 if (response.containsHeader(HttpHeaders.SET_COOKIE)) {
234 long expires = response.getExpires();
235 if (expires == Long.MIN_VALUE || expires >= responseTime) {
236 response.reallySetDateHeader(HttpHeaders.EXPIRES, responseTime);
239 Vector<String> cacheDirectives = new Vector<String>(response.getCacheControl());
240 while (cacheDirectives.remove("public")) {
242 if (!cacheDirectives.contains("private") && !cacheDirectives.contains("no-cache") &&
243 !cacheDirectives.contains("no-store")) {
244 cacheDirectives.add("private");
246 StringBuilder newCacheControl = new StringBuilder();
247 for (String directive : cacheDirectives) {
248 if (newCacheControl.length() > 0) {
249 newCacheControl.append(", ");
251 newCacheControl.append(directive);
253 response.reallySetHeader(HttpHeaders.CACHE_CONTROL, newCacheControl.toString());
258 * Deletes the response body, if X-AppEngine-BlobKey is present.
260 * Otherwise, it would be an error if we were to send text to the client and
261 * then attempt to rewrite the body to serve the blob.
263 * @param request A request object, which is not modified.
264 * @param response A response object, which may be modified.
265 * @param responseTime The timestamp indicating when the response completed.
267 private void blobServeRewriter(HttpServletRequest request, ResponseWrapper response,
268 long responseTime) {
269 if (response.containsHeader(BLOB_KEY_HEADER)) {
270 response.reallyResetBuffer();
275 * Rewrites the Content-Length header.
277 * <p>Even though Content-Length is not a user modifiable header, App Engine
278 * sends a correct Content-Length to the user based on the actual response.
280 * <p>If the request method is HEAD or the response status indicates that the
281 * response should not have a body, the body is deleted instead. The existing
282 * Content-Length header is preserved for HEAD requests.
284 * @param request A request object, which is not modified.
285 * @param response A response object, which may be modified.
286 * @param responseTime The timestamp indicating when the response completed.
288 private void contentLengthRewriter(HttpServletRequest request, ResponseWrapper response,
289 long responseTime) {
290 response.flushPrintWriter();
291 Optional<Integer> responseSize;
292 if (request.getMethod().equals("HEAD")) {
293 response.reallyResetBuffer();
294 responseSize = Optional.absent();
295 } else if (!responseMayHaveBody(response.getStatus())) {
296 response.reallySetHeader(HttpHeaders.CONTENT_LENGTH, null);
297 response.reallyResetBuffer();
298 responseSize = Optional.absent();
299 } else {
300 response.reallySetHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(response.getBodyLength()));
301 responseSize = Optional.of(response.getBodyLength());
303 if (logService != null) {
304 if (responseSize.isPresent()) {
305 logService.registerResponseSize(responseSize.get());
306 } else {
307 logService.clearResponseSize();
312 @Override
313 public void destroy() {
317 * Wraps a request to strip out some of the headers.
319 private static class RequestWrapper extends HttpServletRequestWrapper {
321 * An Enumeration that filters out ignored header names.
323 private static class HeaderFilterEnumeration implements Enumeration<Object> {
324 private final Enumeration<?> allNames;
325 private String nextName;
327 HeaderFilterEnumeration(Enumeration<?> allNames) {
328 this.allNames = allNames;
329 getNextValidName();
332 /** Get the next non-ignored name from allNames and store it in nextName.
334 private void getNextValidName() {
335 while (allNames.hasMoreElements()) {
336 String name = (String) allNames.nextElement();
337 if (validHeader(name)) {
338 nextName = name;
339 return;
342 nextName = null;
345 @Override
346 public boolean hasMoreElements() {
347 return nextName != null;
350 @Override
351 public Object nextElement() {
352 if (nextName == null) {
353 throw new NoSuchElementException();
355 String result = nextName;
356 getNextValidName();
357 return result;
361 public RequestWrapper(HttpServletRequest request) {
362 super(request);
365 private static boolean validHeader(String name) {
366 for (String h : IGNORE_REQUEST_HEADERS) {
367 if (h.equalsIgnoreCase(name)) {
368 return false;
371 return true;
374 @Override
375 public long getDateHeader(String name) {
376 return validHeader(name) ? super.getDateHeader(name) : -1;
379 @Override
380 public String getHeader(String name) {
381 return validHeader(name) ? super.getHeader(name) : null;
384 @Override
385 public Enumeration<?> getHeaders(String name) {
386 if (validHeader(name)) {
387 return super.getHeaders(name);
388 } else {
389 return new Enumeration<Object>() {
390 @Override
391 public boolean hasMoreElements() {
392 return false;
394 @Override
395 public Object nextElement() {
396 throw new NoSuchElementException();
402 @Override
403 public Enumeration<?> getHeaderNames() {
404 return new HeaderFilterEnumeration(super.getHeaderNames());
407 @Override
408 public int getIntHeader(String name) {
409 return validHeader(name) ? super.getIntHeader(name) : -1;
414 * Wraps a response to buffer the entire body, and allow reading of the
415 * status, body and headers.
417 * <p>This buffers the entire body locally, so that the body is not streamed
418 * in chunks to the client, but instead all at the end.
420 * <p>This is necessary to calculate the correct Content-Length at the end,
421 * and also to modify headers after the application returns, but also matches
422 * production behaviour.
424 * <p>For the sake of compatibility, the class <em>pretends</em> not to buffer
425 * any data. (It behaves as if it has a buffer size of 0.) Therefore, as with
426 * a normal {@link HttpServletResponseWrapper}, you may not modify the status
427 * or headers after modifying the body. Note that the {@link PrintWriter}
428 * returned by {@link #getWriter()} does its own limited buffering.
430 * <p>This class also provides the ability to read the value of the status and
431 * some of the headers (which is not available before Servlet 3.0), and the
432 * body.
434 private static class ResponseWrapper extends HttpServletResponseWrapper {
435 private int status = SC_OK;
438 * The value of the Expires header, parsed as a Java timestamp.
440 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
442 private long expires = Long.MIN_VALUE;
443 /** The value of the Cache-Control headers, parsed into separate directives. */
444 private final Vector<String> cacheControl = new Vector<String>();
446 /** A buffer to hold the body without sending it to the client. */
447 private final ByteArrayOutputStream body = new ByteArrayOutputStream();
448 private ServletOutputStream bodyServletStream = null;
449 private PrintWriter bodyPrintWriter = null;
450 /** Indicates that flushBuffer() has been called. */
451 private boolean committed = false;
453 private static final String DATE_FORMAT_STRING =
454 "E, dd MMM yyyy HH:mm:ss 'GMT'";
456 ResponseWrapper(HttpServletResponse response) {
457 super(response);
460 @Override
461 public ServletOutputStream getOutputStream() {
462 if (bodyServletStream != null) {
463 return bodyServletStream;
464 } else {
465 Preconditions.checkState(bodyPrintWriter == null, "getWriter has already been called");
466 bodyServletStream = new ServletOutputStreamWrapper(body);
467 return bodyServletStream;
471 @Override
472 public PrintWriter getWriter() throws UnsupportedEncodingException {
473 if (bodyPrintWriter != null) {
474 return bodyPrintWriter;
475 } else {
476 Preconditions.checkState(bodyServletStream == null,
477 "getOutputStream has already been called");
478 bodyPrintWriter = new PrintWriter(new OutputStreamWriter(body, getCharacterEncoding()));
479 return bodyPrintWriter;
483 @Override
484 public void setCharacterEncoding(String charset) {
485 if (bodyPrintWriter != null || isCommitted()) {
486 return;
488 super.setCharacterEncoding(charset);
491 @Override
492 public void setContentLength(int len) {
493 if (isCommitted()) {
494 return;
496 super.setContentLength(len);
499 @Override
500 public void setContentType(String type) {
501 if (isCommitted()) {
502 return;
504 if (type != null && nonAscii(type)) {
505 return;
507 if (bodyPrintWriter != null) {
508 type = stripCharsetFromMediaType(type);
510 super.setContentType(type);
513 @Override
514 public void setLocale(Locale loc) {
515 if (isCommitted()) {
516 return;
518 String oldCharacterEncoding = getCharacterEncoding();
519 String oldContentType = getContentType();
520 super.setLocale(loc);
521 if (oldContentType != null || bodyPrintWriter != null) {
522 super.setCharacterEncoding(oldCharacterEncoding);
524 if (oldContentType != null) {
525 super.setContentType(oldContentType);
529 @Override
530 public void setBufferSize(int size) {
531 checkNotCommitted();
532 super.setBufferSize(size);
535 @Override
536 public int getBufferSize() {
537 return 0;
540 @Override
541 public void flushBuffer() {
542 committed = true;
545 @Override
546 public void reset() {
547 checkNotCommitted();
548 super.reset();
551 @Override
552 public void resetBuffer() {
553 checkNotCommitted();
556 @Override
557 public boolean isCommitted() {
558 return committed || body.size() > 0;
562 * Checks whether {@link #isCommitted()} is true, and if so, raises
563 * {@link IllegalStateException}.
565 void checkNotCommitted() {
566 Preconditions.checkState(!isCommitted(), "Response has already been committed");
569 @Override
570 public void addCookie(Cookie cookie) {
571 if (isCommitted()) {
572 return;
574 super.addCookie(cookie);
577 @Override
578 public void addDateHeader(String name, long date) {
579 if (isCommitted()) {
580 return;
582 if (nonAscii(name)) {
583 return;
585 super.addDateHeader(name, date);
586 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
587 expires = date;
591 @Override
592 public void addHeader(String name, String value) {
593 if (isCommitted()) {
594 return;
596 if (value == null) {
597 return;
599 if (nonAscii(name) || nonAscii(value)) {
600 return;
602 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
603 try {
604 parseExpires(value);
605 } catch (ParseException e) {
607 } else if (name.equalsIgnoreCase(HttpHeaders.CACHE_CONTROL)) {
608 parseCacheControl(value);
609 } else if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
610 if (bodyPrintWriter != null) {
611 value = stripCharsetFromMediaType(value);
614 super.addHeader(name, value);
617 @Override
618 public void addIntHeader(String name, int value) {
619 if (isCommitted()) {
620 return;
622 if (nonAscii(name)) {
623 return;
625 super.addIntHeader(name, value);
628 @Override
629 public void sendError(int sc) throws IOException {
630 checkNotCommitted();
631 setStatus(sc);
632 setErrorBody(Integer.toString(sc));
635 @Override
636 public void sendError(int sc, String msg) throws IOException {
637 checkNotCommitted();
638 setStatus(sc, msg);
639 setErrorBody(sc + " " + msg);
642 /** Sets the response body to an HTML page with an error message.
644 * This also sets the Content-Type header.
646 * @param errorText A message to display in the title and page contents.
647 * Should contain an HTTP status code and optional message.
649 private void setErrorBody(String errorText) throws IOException {
650 setHeader(HttpHeaders.CONTENT_TYPE, "text/html; charset=iso-8859-1");
651 String bodyText = "<html><head><title>Error " + errorText + "</title></head>\n"
652 + "<body><h2>Error " + errorText + "</h2></body>\n"
653 + "</html>";
654 body.write(bodyText.getBytes("iso-8859-1"));
657 @Override
658 public void sendRedirect(String location) {
659 checkNotCommitted();
660 setStatus(SC_FOUND);
661 resetBuffer();
662 setHeader(HttpHeaders.LOCATION, encodeRedirectURL(location));
663 status = SC_FOUND;
666 @Override
667 public void setDateHeader(String name, long date) {
668 if (isCommitted()) {
669 return;
671 if (nonAscii(name)) {
672 return;
674 reallySetDateHeader(name, date);
677 @Override
678 public void setHeader(String name, String value) {
679 if (isCommitted()) {
680 return;
682 if (nonAscii(name) || (value != null && nonAscii(value))) {
683 return;
685 if (name.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
686 if (bodyPrintWriter != null) {
687 value = stripCharsetFromMediaType(value);
690 reallySetHeader(name, value);
693 @Override
694 public void setIntHeader(String name, int value) {
695 if (isCommitted()) {
696 return;
698 if (nonAscii(name)) {
699 return;
701 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
702 expires = Long.MIN_VALUE;
704 super.setIntHeader(name, value);
707 @Override
708 public void setStatus(int sc) {
709 if (isCommitted()) {
710 return;
712 super.setStatus(sc);
713 status = sc;
716 @Override
717 public void setStatus(int sc, String sm) {
718 if (isCommitted()) {
719 return;
721 super.setStatus(sc, sm);
722 status = sc;
726 * Gets the status code of the response.
728 int getStatus() {
729 return status;
733 * Gets the value of the Expires header, as a Java timestamp.
735 * <p>Long.MIN_VALUE indicates that the Expires header is missing or invalid.
737 public long getExpires() {
738 return expires;
742 * Gets the value of the Cache-Control headers, parsed into separate directives.
744 public Vector<String> getCacheControl() {
745 return cacheControl;
749 * Gets the total number of bytes that have been written to the body without
750 * committing.
752 int getBodyLength() {
753 return body.size();
757 * Writes the body to the wrapped response's output stream.
759 * <p>If the body is not empty, this causes the status and headers to be
760 * rewritten. This should not be called until all of the header and body
761 * rewriting is complete.
763 * <p>If the body is empty, this has no effect, so the response can be
764 * considered not committed.
766 void reallyCommit() throws IOException {
767 flushPrintWriter();
768 if (!isCommitted()) {
769 return;
771 OutputStream stream = super.getOutputStream();
772 stream.write(body.toByteArray());
773 body.reset();
777 * Reset the output buffer.
779 * This works even though {@link #isCommitted()} may return true.
781 void reallyResetBuffer() {
782 body.reset();
783 bodyServletStream = null;
784 bodyPrintWriter = null;
788 * Sets a header in the response.
790 * This works even though {@link #isCommitted()} may return true.
792 void reallySetHeader(String name, String value) {
793 super.setHeader(name, value);
794 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
795 if (value == null) {
796 expires = Long.MIN_VALUE;
797 } else {
798 try {
799 parseExpires(value);
800 } catch (ParseException e) {
801 expires = Long.MIN_VALUE;
804 } else if (name.equalsIgnoreCase(HttpHeaders.CACHE_CONTROL)) {
805 cacheControl.clear();
806 if (value != null) {
807 parseCacheControl(value);
813 * Sets a date header in the response.
815 * This works even though {@link #isCommitted()} may return true.
817 void reallySetDateHeader(String name, long date) {
818 super.setDateHeader(name, date);
819 if (name.equalsIgnoreCase(HttpHeaders.EXPIRES)) {
820 expires = date;
825 * Flushes the {@link PrintWriter} returned by {@link #getWriter()}, if it
826 * exists.
828 void flushPrintWriter() {
829 if (bodyPrintWriter != null) {
830 bodyPrintWriter.flush();
835 * Parse a date string and store the result in expires.
837 private void parseExpires(String date) throws ParseException {
838 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_STRING);
839 dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
840 Date parsedDate = dateFormat.parse(date);
841 expires = parsedDate.getTime();
845 * Parse a comma-separated list, and add the items to cacheControl.
847 private void parseCacheControl(String directives) {
848 String[] elements = directives.split(",");
849 for (String element : elements) {
850 cacheControl.add(element.trim());
855 * Removes the charset parameter from a media type string.
857 * @param mediaType A media type string, such as a Content-Type value.
858 * @return The media type with the charset parameter removed, if any
859 * existed. If not, returns the media type unchanged.
861 private static String stripCharsetFromMediaType(String mediaType) {
862 String newMediaType = null;
863 for (String part : mediaType.split(";")) {
864 part = part.trim();
865 if (!(part.length() >= 8 &&
866 part.substring(0, 8).equalsIgnoreCase("charset="))) {
867 newMediaType = newMediaType == null ? "" : newMediaType + "; ";
868 newMediaType += part;
871 return newMediaType;
875 * Tests whether a string contains any non-ASCII characters.
877 private static boolean nonAscii(String string) {
878 for (char c : string.toCharArray()) {
879 if (c >= 0x80) {
880 return true;
883 return false;
887 * A ServletOutputStream that wraps some other OutputStream.
889 private static class ServletOutputStreamWrapper extends ServletOutputStream {
890 private final OutputStream stream;
892 ServletOutputStreamWrapper(OutputStream stream) {
893 this.stream = stream;
896 @Override
897 public void close() throws IOException {
898 stream.close();
901 @Override
902 public void flush() throws IOException {
903 stream.flush();
906 @Override
907 public void write(byte[] b) throws IOException {
908 stream.write(b);
911 @Override
912 public void write(byte[] b, int off, int len) throws IOException {
913 stream.write(b, off, len);
916 @Override
917 public void write(int b) throws IOException {
918 stream.write(b);