001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://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, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.user.server.rpc;
017:
018: import java.io.ByteArrayOutputStream;
019: import java.io.IOException;
020: import java.io.InputStream;
021: import java.util.zip.GZIPOutputStream;
022:
023: import javax.servlet.ServletContext;
024: import javax.servlet.ServletException;
025: import javax.servlet.http.HttpServletRequest;
026: import javax.servlet.http.HttpServletResponse;
027:
028: /**
029: * Utility class containing helper methods used by servlets that integrate with
030: * the RPC system.
031: */
032: public class RPCServletUtils {
033: private static final String ACCEPT_ENCODING = "Accept-Encoding";
034:
035: private static final String CHARSET_UTF8 = "UTF-8";
036:
037: private static final String CONTENT_ENCODING = "Content-Encoding";
038:
039: private static final String CONTENT_ENCODING_GZIP = "gzip";
040:
041: private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8";
042:
043: private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
044:
045: /**
046: * Controls the compression threshold at and below which no compression will
047: * take place.
048: */
049: private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;
050:
051: /**
052: * Returns <code>true</code> if the {@link HttpServletRequest} accepts Gzip
053: * encoding. This is done by checking that the accept-encoding header
054: * specifies gzip as a supported encoding.
055: *
056: * @param request the request instance to test for gzip encoding acceptance
057: * @return <code>true</code> if the {@link HttpServletRequest} accepts Gzip
058: * encoding
059: */
060: public static boolean acceptsGzipEncoding(HttpServletRequest request) {
061: assert (request != null);
062:
063: String acceptEncoding = request.getHeader(ACCEPT_ENCODING);
064: if (null == acceptEncoding) {
065: return false;
066: }
067:
068: return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1);
069: }
070:
071: /**
072: * Returns <code>true</code> if the response content's estimated UTF-8 byte
073: * length exceeds 256 bytes.
074: *
075: * @param content the contents of the response
076: * @return <code>true</code> if the response content's estimated UTF-8 byte
077: * length exceeds 256 bytes
078: */
079: public static boolean exceedsUncompressedContentLengthLimit(
080: String content) {
081: return (content.length() * 2) > UNCOMPRESSED_BYTE_SIZE_LIMIT;
082: }
083:
084: /**
085: * Returns the content of an {@link HttpServletRequest} by decoding it using
086: * the UTF-8 charset.
087: *
088: * @param request the servlet request whose content we want to read
089: * @return the content of an {@link HttpServletRequest} by decoding it using
090: * the UTF-8 charset
091: * @throws IOException if the requests input stream cannot be accessed, read
092: * from or closed
093: * @throws ServletException if the content length of the request is not
094: * specified of if the request's content type is not 'text/plain' or
095: * 'charset=utf-8'
096: */
097: public static String readContentAsUtf8(HttpServletRequest request)
098: throws IOException, ServletException {
099: int contentLength = request.getContentLength();
100: if (contentLength == -1) {
101: // Content length must be known.
102: throw new ServletException(
103: "Content-Length must be specified");
104: }
105:
106: String contentType = request.getContentType();
107: boolean contentTypeIsOkay = false;
108: // Content-Type must be specified.
109: if (contentType != null) {
110: contentType = contentType.toLowerCase();
111: // The type must be plain text.
112: if (contentType.startsWith("text/plain")) {
113: // And it must be UTF-8 encoded (or unspecified, in which case we assume
114: // that it's either UTF-8 or ASCII).
115: if (contentType.indexOf("charset=") == -1) {
116: contentTypeIsOkay = true;
117: } else if (contentType.indexOf("charset=utf-8") != -1) {
118: contentTypeIsOkay = true;
119: }
120: }
121: }
122:
123: if (!contentTypeIsOkay) {
124: throw new ServletException(
125: "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)");
126: }
127:
128: InputStream in = request.getInputStream();
129: try {
130: byte[] payload = new byte[contentLength];
131: int offset = 0;
132: int len = contentLength;
133: int byteCount;
134: while (offset < contentLength) {
135: byteCount = in.read(payload, offset, len);
136: if (byteCount == -1) {
137: throw new ServletException("Client did not send "
138: + contentLength + " bytes as expected");
139: }
140: offset += byteCount;
141: len -= byteCount;
142: }
143: return new String(payload, CHARSET_UTF8);
144: } finally {
145: if (in != null) {
146: in.close();
147: }
148: }
149: }
150:
151: /**
152: * Returns <code>true</code> if the request accepts gzip encoding and the
153: * the response content's estimated UTF-8 byte length exceeds 256 bytes.
154: *
155: * @param request the request associated with the response content
156: * @param responseContent a string that will be
157: * @return <code>true</code> if the request accepts gzip encoding and the
158: * the response content's estimated UTF-8 byte length exceeds 256
159: * bytes
160: */
161: public static boolean shouldGzipResponseContent(
162: HttpServletRequest request, String responseContent) {
163: return acceptsGzipEncoding(request)
164: && exceedsUncompressedContentLengthLimit(responseContent);
165: }
166:
167: /**
168: * Write the response content into the {@link HttpServletResponse}. If
169: * <code>gzipResponse</code> is <code>true</code>, the response content
170: * will be gzipped prior to being written into the response.
171: *
172: * @param servletContext servlet context for this response
173: * @param response response instance
174: * @param responseContent a string containing the response content
175: * @param gzipResponse if <code>true</code> the response content will be
176: * gzip encoded before being written into the response
177: * @throws IOException if reading, writing, or closing the response's output
178: * stream fails
179: */
180: public static void writeResponse(ServletContext servletContext,
181: HttpServletResponse response, String responseContent,
182: boolean gzipResponse) throws IOException {
183:
184: byte[] responseBytes = responseContent.getBytes(CHARSET_UTF8);
185: if (gzipResponse) {
186: // Compress the reply and adjust headers.
187: //
188: ByteArrayOutputStream output = null;
189: GZIPOutputStream gzipOutputStream = null;
190: Throwable caught = null;
191: try {
192: output = new ByteArrayOutputStream(responseBytes.length);
193: gzipOutputStream = new GZIPOutputStream(output);
194: gzipOutputStream.write(responseBytes);
195: gzipOutputStream.finish();
196: gzipOutputStream.flush();
197: response.setHeader(CONTENT_ENCODING,
198: CONTENT_ENCODING_GZIP);
199: responseBytes = output.toByteArray();
200: } catch (IOException e) {
201: caught = e;
202: } finally {
203: if (null != gzipOutputStream) {
204: gzipOutputStream.close();
205: }
206: if (null != output) {
207: output.close();
208: }
209: }
210:
211: if (caught != null) {
212: servletContext.log("Unable to compress response",
213: caught);
214: response
215: .sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
216: return;
217: }
218: }
219:
220: // Send the reply.
221: //
222: response.setContentLength(responseBytes.length);
223: response.setContentType(CONTENT_TYPE_TEXT_PLAIN_UTF8);
224: response.setStatus(HttpServletResponse.SC_OK);
225: response.getOutputStream().write(responseBytes);
226: }
227:
228: /**
229: * Called when the servlet itself has a problem, rather than the invoked
230: * third-party method. It writes a simple 500 message back to the client.
231: *
232: * @param servletContext
233: * @param response
234: * @param failure
235: */
236: public static void writeResponseForUnexpectedFailure(
237: ServletContext servletContext,
238: HttpServletResponse response, Throwable failure) {
239: servletContext.log(
240: "Exception while dispatching incoming RPC call",
241: failure);
242:
243: // Send GENERIC_FAILURE_MSG with 500 status.
244: //
245: try {
246: response.setContentType("text/plain");
247: response
248: .setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
249: response.getWriter().write(GENERIC_FAILURE_MSG);
250: } catch (IOException ex) {
251: servletContext
252: .log(
253: "respondWithUnexpectedFailure failed while sending the previous failure to the client",
254: ex);
255: }
256: }
257:
258: private RPCServletUtils() {
259: // Not instantiable
260: }
261: }
|