KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > google > gwt > user > server > rpc > RemoteServiceServlet


1 /*
2  * Copyright 2007 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */

16 package com.google.gwt.user.server.rpc;
17
18 import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
19 import com.google.gwt.user.client.rpc.SerializationException;
20
21 import java.io.ByteArrayOutputStream JavaDoc;
22 import java.io.IOException JavaDoc;
23 import java.io.InputStream JavaDoc;
24 import java.util.zip.GZIPOutputStream JavaDoc;
25
26 import javax.servlet.ServletContext JavaDoc;
27 import javax.servlet.ServletException JavaDoc;
28 import javax.servlet.http.HttpServlet JavaDoc;
29 import javax.servlet.http.HttpServletRequest JavaDoc;
30 import javax.servlet.http.HttpServletResponse JavaDoc;
31
32 /**
33  * The servlet base class for your RPC service implementations that
34  * automatically deserializes incoming requests from the client and serializes
35  * outgoing responses for client/server RPCs.
36  */

37 public class RemoteServiceServlet extends HttpServlet JavaDoc {
38   /*
39    * These members are used to get and set the different HttpServletResponse and
40    * HttpServletRequest headers.
41    */

42   private static final String JavaDoc ACCEPT_ENCODING = "Accept-Encoding";
43   private static final String JavaDoc CHARSET_UTF8 = "UTF-8";
44   private static final String JavaDoc CONTENT_ENCODING = "Content-Encoding";
45   private static final String JavaDoc CONTENT_ENCODING_GZIP = "gzip";
46   private static final String JavaDoc CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8";
47   private static final String JavaDoc GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
48
49   /**
50    * Controls the compression threshold at and below which no compression will
51    * take place.
52    */

53   private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;
54
55   /**
56    * Return true if the response object accepts Gzip encoding. This is done by
57    * checking that the accept-encoding header specifies gzip as a supported
58    * encoding.
59    */

60   private static boolean acceptsGzipEncoding(HttpServletRequest JavaDoc request) {
61     assert (request != null);
62
63     String JavaDoc acceptEncoding = request.getHeader(ACCEPT_ENCODING);
64     if (null == acceptEncoding) {
65       return false;
66     }
67
68     return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1);
69   }
70
71   /**
72    * This method attempts to estimate the number of bytes that a string will
73    * consume when it is sent out as part of an HttpServletResponse. This really
74    * a hack since we are assuming that every character will consume two bytes
75    * upon transmission. This is definitely not true since some characters
76    * actually consume more than two bytes and some consume less. This is even
77    * less accurate if the string is converted to UTF8. However, it does save us
78    * from converting every string that we plan on sending back to UTF8 just to
79    * determine that we should not compress it.
80    */

81   private static int estimateByteSize(final String JavaDoc buffer) {
82     return (buffer.length() * 2);
83   }
84
85   /**
86    * Read the payload as UTF-8 from the request stream.
87    */

88   private static String JavaDoc readPayloadAsUtf8(HttpServletRequest JavaDoc request)
89       throws IOException JavaDoc, ServletException JavaDoc {
90     int contentLength = request.getContentLength();
91     if (contentLength == -1) {
92       // Content length must be known.
93
throw new ServletException JavaDoc("Content-Length must be specified");
94     }
95
96     String JavaDoc contentType = request.getContentType();
97     boolean contentTypeIsOkay = false;
98     // Content-Type must be specified.
99
if (contentType != null) {
100       // The type must be plain text.
101
if (contentType.startsWith("text/plain")) {
102         // And it must be UTF-8 encoded (or unspecified, in which case we assume
103
// that it's either UTF-8 or ASCII).
104
if (contentType.indexOf("charset=") == -1) {
105           contentTypeIsOkay = true;
106         } else if (contentType.indexOf("charset=utf-8") != -1) {
107           contentTypeIsOkay = true;
108         }
109       }
110     }
111     if (!contentTypeIsOkay) {
112       throw new ServletException JavaDoc(
113           "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)");
114     }
115     InputStream JavaDoc in = request.getInputStream();
116     try {
117       byte[] payload = new byte[contentLength];
118       int offset = 0;
119       int len = contentLength;
120       int byteCount;
121       while (offset < contentLength) {
122         byteCount = in.read(payload, offset, len);
123         if (byteCount == -1) {
124           throw new ServletException JavaDoc("Client did not send " + contentLength
125               + " bytes as expected");
126         }
127         offset += byteCount;
128         len -= byteCount;
129       }
130       return new String JavaDoc(payload, "UTF-8");
131     } finally {
132       if (in != null) {
133         in.close();
134       }
135     }
136   }
137
138   private final ThreadLocal JavaDoc perThreadRequest = new ThreadLocal JavaDoc();
139
140   private final ThreadLocal JavaDoc perThreadResponse = new ThreadLocal JavaDoc();
141
142   /**
143    * The default constructor.
144    */

145   public RemoteServiceServlet() {
146   }
147
148   /**
149    * Standard HttpServlet method: handle the POST.
150    *
151    * This doPost method swallows ALL exceptions, logs them in the
152    * ServletContext, and returns a GENERIC_FAILURE_MSG response with status code
153    * 500.
154    */

155   public final void doPost(HttpServletRequest JavaDoc request,
156       HttpServletResponse JavaDoc response) {
157     try {
158       // Store the request & response objects in thread-local storage.
159
//
160
perThreadRequest.set(request);
161       perThreadResponse.set(response);
162
163       // Read the request fully.
164
//
165
String JavaDoc requestPayload = readPayloadAsUtf8(request);
166
167       // Let subclasses see the serialized request.
168
//
169
onBeforeRequestDeserialized(requestPayload);
170
171       // Invoke the core dispatching logic, which returns the serialized
172
// result.
173
//
174
String JavaDoc responsePayload = processCall(requestPayload);
175
176       // Let subclasses see the serialized response.
177
//
178
onAfterResponseSerialized(responsePayload);
179
180       // Write the response.
181
//
182
writeResponse(request, response, responsePayload);
183       return;
184     } catch (Throwable JavaDoc e) {
185       // Give a subclass a chance to either handle the exception or rethrow it
186
//
187
doUnexpectedFailure(e);
188     } finally {
189       // null the thread-locals to avoid holding request/response
190
//
191
perThreadRequest.set(null);
192       perThreadResponse.set(null);
193     }
194   }
195
196   /**
197    * Process a call originating from the given request. Uses the
198    * {@link RPC#invokeAndEncodeResponse(Object, java.lang.reflect.Method, Object[])}
199    * method to do the actual work.
200    * <p>
201    * Subclasses may optionally override this method to handle the payload in any
202    * way they desire (by routing the request to a framework component, for
203    * instance). The {@link HttpServletRequest} and {@link HttpServletResponse}
204    * can be accessed via the {@link #getThreadLocalRequest()} and
205    * {@link #getThreadLocalResponse()} methods.
206    * </p>
207    * This is public so that it can be unit tested easily without HTTP.
208    *
209    * @param payload the UTF-8 request payload
210    * @return a string which encodes either the method's return, a checked
211    * exception thrown by the method, or an
212    * {@link IncompatibleRemoteServiceException}
213    * @throws SerializationException if we cannot serialize the response
214    * @throws UnexpectedException if the invocation throws a checked exception
215    * that is not declared in the service method's signature
216    * @throws RuntimeException if the service method throws an unchecked
217    * exception (the exception will be the one thrown by the service)
218    */

219   public String JavaDoc processCall(String JavaDoc payload) throws SerializationException {
220     try {
221       RPCRequest rpcRequest = RPC.decodeRequest(payload, this.getClass());
222       return RPC.invokeAndEncodeResponse(this, rpcRequest.getMethod(),
223           rpcRequest.getParameters());
224     } catch (IncompatibleRemoteServiceException ex) {
225       return RPC.encodeResponseForFailure(null, ex);
226     }
227   }
228
229   /**
230    * Override this method to control what should happen when an exception
231    * escapes the {@link #processCall(String)} method. The default implementation
232    * will log the failure and send a generic failure response to the client.<p/>
233    *
234    * An "expected failure" is an exception thrown by a service method that is
235    * declared in the signature of the service method. These exceptions are
236    * serialized back to the client, and are not passed to this method. This
237    * method is called only for exceptions or errors that are not part of the
238    * service method's signature, or that result from SecurityExceptions,
239    * SerializationExceptions, or other failures within the RPC framework.<p/>
240    *
241    * Note that if the desired behavior is to both send the GENERIC_FAILURE_MSG
242    * response AND to rethrow the exception, then this method should first send
243    * the GENERIC_FAILURE_MSG response itself (using getThreadLocalResponse), and
244    * then rethrow the exception. Rethrowing the exception will cause it to
245    * escape into the servlet container.
246    *
247    * @param e the exception which was thrown
248    */

249   protected void doUnexpectedFailure(Throwable JavaDoc e) {
250     ServletContext JavaDoc servletContext = getServletContext();
251     servletContext.log("Exception while dispatching incoming RPC call", e);
252
253     // Send GENERIC_FAILURE_MSG with 500 status.
254
//
255
respondWithFailure(getThreadLocalResponse());
256   }
257
258   /**
259    * Gets the <code>HttpServletRequest</code> object for the current call. It
260    * is stored thread-locally so that simultaneous invocations can have
261    * different request objects.
262    */

263   protected final HttpServletRequest JavaDoc getThreadLocalRequest() {
264     return (HttpServletRequest JavaDoc) perThreadRequest.get();
265   }
266
267   /**
268    * Gets the <code>HttpServletResponse</code> object for the current call. It
269    * is stored thread-locally so that simultaneous invocations can have
270    * different response objects.
271    */

272   protected final HttpServletResponse JavaDoc getThreadLocalResponse() {
273     return (HttpServletResponse JavaDoc) perThreadResponse.get();
274   }
275
276   /**
277    * Override this method to examine the serialized response that will be
278    * returned to the client. The default implementation does nothing and need
279    * not be called by subclasses.
280    */

281   protected void onAfterResponseSerialized(String JavaDoc serializedResponse) {
282   }
283
284   /**
285    * Override this method to examine the serialized version of the request
286    * payload before it is deserialized into objects. The default implementation
287    * does nothing and need not be called by subclasses.
288    */

289   protected void onBeforeRequestDeserialized(String JavaDoc serializedRequest) {
290   }
291
292   /**
293    * Determines whether the response to a given servlet request should or should
294    * not be GZIP compressed. This method is only called in cases where the
295    * requestor accepts GZIP encoding.
296    * <p>
297    * This implementation currently returns <code>true</code> if the response
298    * string's estimated byte length is longer than 256 bytes. Subclasses can
299    * override this logic.
300    * </p>
301    *
302    * @param request the request being served
303    * @param response the response that will be written into
304    * @param responsePayload the payload that is about to be sent to the client
305    * @return <code>true</code> if responsePayload should be GZIP compressed,
306    * otherwise <code>false</code>.
307    */

308   protected boolean shouldCompressResponse(HttpServletRequest JavaDoc request,
309       HttpServletResponse JavaDoc response, String JavaDoc responsePayload) {
310     return estimateByteSize(responsePayload) > UNCOMPRESSED_BYTE_SIZE_LIMIT;
311   }
312
313   /**
314    * Called when the machinery of this class itself has a problem, rather than
315    * the invoked third-party method. It writes a simple 500 message back to the
316    * client.
317    */

318   private void respondWithFailure(HttpServletResponse JavaDoc response) {
319     try {
320       response.setContentType("text/plain");
321       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
322       response.getWriter().write(GENERIC_FAILURE_MSG);
323     } catch (IOException JavaDoc e) {
324       getServletContext().log(
325           "respondWithFailure failed while sending the previous failure to the client",
326           e);
327     }
328   }
329
330   /**
331    * Write the response payload to the response stream.
332    */

333   private void writeResponse(HttpServletRequest JavaDoc request,
334       HttpServletResponse JavaDoc response, String JavaDoc responsePayload) throws IOException JavaDoc {
335
336     byte[] reply = responsePayload.getBytes(CHARSET_UTF8);
337     String JavaDoc contentType = CONTENT_TYPE_TEXT_PLAIN_UTF8;
338
339     if (acceptsGzipEncoding(request)
340         && shouldCompressResponse(request, response, responsePayload)) {
341       // Compress the reply and adjust headers.
342
//
343
ByteArrayOutputStream JavaDoc output = null;
344       GZIPOutputStream JavaDoc gzipOutputStream = null;
345       Throwable JavaDoc caught = null;
346       try {
347         output = new ByteArrayOutputStream JavaDoc(reply.length);
348         gzipOutputStream = new GZIPOutputStream JavaDoc(output);
349         gzipOutputStream.write(reply);
350         gzipOutputStream.finish();
351         gzipOutputStream.flush();
352         response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP);
353         reply = output.toByteArray();
354       } catch (IOException JavaDoc e) {
355         caught = e;
356       } finally {
357         if (null != gzipOutputStream) {
358           gzipOutputStream.close();
359         }
360         if (null != output) {
361           output.close();
362         }
363       }
364
365       if (caught != null) {
366         getServletContext().log("Unable to compress response", caught);
367         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
368         return;
369       }
370     }
371
372     // Send the reply.
373
//
374
response.setContentLength(reply.length);
375     response.setContentType(contentType);
376     response.setStatus(HttpServletResponse.SC_OK);
377     response.getOutputStream().write(reply);
378   }
379 }
380
Popular Tags