Contribution: Spring GWT Controller

41 views
Skip to first unread message

oktobermann

unread,
Jul 11, 2006, 3:33:21 AM7/11/06
to Google Web Toolkit
Happily copy & pasted from the GWT RemoteServiceServlet.
Place either in com.google.gwt.user.server.rpc or make the following
classes public:
ServerSerializableTypeOracle, ServerSerializableTypeOracleImpl

<code>
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStream;
import com.google.gwt.user.server.rpc.ServerSerializationStream;
import com.google.gwt.user.server.rpc.ServerSerializableTypeOracle;
import com.google.gwt.user.server.rpc.ServerSerializableTypeOracleImpl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*
* Copyright 2006 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
may not
* use this file except in compliance with the License. You may obtain
a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
the
* License for the specific language governing permissions and
limitations under
* the License.
*
* Copied from the GWT servlet
*
*/

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStream;
import com.google.gwt.user.server.rpc.ServerSerializationStream;
import com.google.gwt.user.server.rpc.ServerSerializableTypeOracle;
import com.google.gwt.user.server.rpc.ServerSerializableTypeOracleImpl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public abstract class GWTSpringController extends AbstractController {

/**
* The servlet base class for your RPC service implementations that
* automatically deserializes incoming requests from the client and
* serializes outgoing responses for client/server RPCs.
*/
public interface CallResponse {
void onDeclaredExceptionThrown(Throwable e);

void onUnexpectedExceptionThrown(Throwable e);

void onNormalResult(String responsePayload);
}

private static final String ACCEPT_ENCODING = "Accept-Encoding";

private static final String CHARSET_UTF8 = "UTF-8";

/**
* These members are used to get and set the different
HttpServletResponse
* and HttpServletRequest headers.
*/
private static final String CONTENT_ENCODING = "Content-Encoding";

private static final String CONTENT_ENCODING_GZIP = "gzip";

private static final String CONTENT_TYPE = "Content-Type";

private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 =
"text/plain; charset=utf-8";

private static final String GENERIC_FAILURE_MSG = "The call failed on
the server; see server log for details";

private static final HashMap TYPE_NAMES;

/**
* Controls the compression threshold at and below which no
compression will
* take place.
*/
private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;

/**
* Find the invoked method on either the specified interface or any
super.
*/
private static Method findInterfaceMethod(Class intf, String
methodName, Class[] paramTypes,
boolean includeInherited) {
try {
return intf.getDeclaredMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
if (includeInherited) {
Class[] superintfs = intf.getInterfaces();
for (int i = 0; i < superintfs.length; i++) {
Method method = findInterfaceMethod(superintfs[i], methodName,
paramTypes, true);
if (method != null)
return method;
}
}

return null;
}
}

/**
* Return true if the response object accepts Gzip encoding. This is
done by
* checking that the accept-encoding header specifies gzip as a
supported
* encoding.
*/
private static boolean acceptsGzipEncoding(HttpServletRequest request)
{
assert (request != null);

String acceptEncoding = request.getHeader(ACCEPT_ENCODING);
if (null == acceptEncoding) {
return false;
}

return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1);
}

/**
* This method attempts to estimate the number of bytes that a string
will
* consume when it is sent out as part of an HttpServletResponse.
*
* This really a hack since we are assuming that every character will
* consume two bytes upon transmission. This is definitely not true
since
* some characters actually consume more than two bytes and some
consume
* less. This is even less accurate if the string is converted to
UTF8.
* However, it does save us from converting every string that we plan
on
* sending back to UTF8 just to determine that we should not compress
it.
*/
private static int estimateByteSize(final String buffer) {
return (buffer.length() * 2);
}

private Class getClassFromName(String name) throws
ClassNotFoundException {
Object value = TYPE_NAMES.get(name);
if (value != null) {
return (Class) value;
}

return Class.forName(name, false, this.getClass().getClassLoader());
}

/**
* This method returns true if the response to the http request should
be
* compressed. This is done if the request accepts GZIP and the
response
* string's estimated byte length is longer than 128 bytes.
*/
private static boolean shouldCompress(final HttpServletRequest
request, final String buffer) {

if (acceptsGzipEncoding(request)) {
return estimateByteSize(buffer) > UNCOMPRESSED_BYTE_SIZE_LIMIT;
}

return false;
}

/**
* The default constructor.
*
* @skip
*/
public GWTSpringController() {
serializableTypeOracle = new
ServerSerializableTypeOracleImpl(getPackagePaths());
}

/**
* This is public so that it can be unit tested easily without HTTP.
*
* @skip
*/
public String processCall(String payload) throws
SerializationException {

// Let subclasses see the serialized request.
//
onBeforeRequestDeserialized(payload);

// Create a stream to deserialize the request.
//
ServerSerializationStream stream = new
ServerSerializationStream(serializableTypeOracle);
stream.prepareToRead(payload);

// Read the service interface
//
String serviceIntfName = (String) stream.readObject();

// TODO(mmendez): need to check the signature
// Verify that this very servlet implements the specified interface
// name.
//
if (!isImplementedRemoteServiceInterface(serviceIntfName)) {
// Bad payload, possible hack attempt.
//
throw new SecurityException(
"Blocked attempt to access interface '"
+ serviceIntfName
+ "', which is either not implemented by this servlet or which
doesn't extend RemoteService; this is either misconfiguration or a hack
attempt");
}

// Actually get the service interface, so that we can query its
methods.
//
Class serviceIntf;
try {
serviceIntf = getClassFromName(serviceIntfName);
} catch (ClassNotFoundException e) {
throw new SerializationException("Unknown service interface class '"
+ serviceIntfName + "'", e);
}

// Read the method name.
//
String methodName = (String) stream.readObject();

// Read the number and names of the parameter classes from the
stream.
// We have to do this so that we can find the correct overload of the
// method.
//
int paramCount = stream.readInt();
Class[] paramTypes = new Class[paramCount];
for (int i = 0; i < paramTypes.length; i++) {
String paramClassName = (String) stream.readObject();
try {
paramTypes[i] = getClassFromName(paramClassName);
} catch (ClassNotFoundException e) {
throw new SerializationException("Unknown parameter " + i + " type
'" + paramClassName + "'", e);
}
}

// For security, make sure the method is found in the service
interface
// and not just one that happens to be defined on this class.
//
Method serviceIntfMethod = findInterfaceMethod(serviceIntf,
methodName, paramTypes, true);

// If it wasn't found, don't continue.
//
if (serviceIntfMethod == null) {
// Bad payload, possible hack attempt.
//
throw new SecurityException("Method '" + methodName + "' (or a
particular overload) on interface '"
+ serviceIntfName + "' was not found, this is either
misconfiguration or a hack attempt");
}

// Deserialize the parameters.
//
Object[] args = new Object[paramCount];
for (int i = 0; i < args.length; i++) {
args[i] = stream.deserializeValue(paramTypes[i]);
}

// Make the call via reflection.
//
String responsePayload = GENERIC_FAILURE_MSG;
Throwable caught = null;
try {
Class returnType = serviceIntfMethod.getReturnType();
Object returnVal = serviceIntfMethod.invoke(this, args);
responsePayload = createResponse(stream, returnType, returnVal,
false);
} catch (IllegalArgumentException e) {
caught = e;
} catch (IllegalAccessException e) {
caught = e;
} catch (InvocationTargetException e) {
caught = e;
}

if (caught != null) {
responsePayload = GENERIC_FAILURE_MSG;

// Be more specific if it's a declared exception.
//
if (caught instanceof InvocationTargetException) {
Throwable cause = ((InvocationTargetException) caught).getCause();
if (cause != null) {
if (isExpectedException(serviceIntfMethod, cause)) {
Class thrownClass = cause.getClass();
responsePayload = createResponse(stream, thrownClass, cause,
true);
}
}
}
}

// Let subclasses see the serialized response.
//
onAfterResponseSerialized(responsePayload);

return responsePayload;
}

/**
* Computes all the possible types that could be needed to resolve the
* overload of a given method on the specified interface. Used for
security
* checks.
*/
private Set computeAllowedTypesForMethodOverload(Class serviceIntf,
String methodName) {
Method[] methods = serviceIntf.getMethods();
Set allParamTypes = new HashSet();
for (int i = 0; i < methods.length; i++) {
Method method = methods[i];
if (method.getName().equals(methodName)) {
Class[] paramTypes = method.getParameterTypes();
for (int j = 0; j < paramTypes.length; j++) {
Class paramType = paramTypes[j];
allParamTypes.add(paramType);
}
}
}
return allParamTypes;
}

/**
* Used to determine whether the specified interface name is
implemented by
* this class without loading the class (for security).
*/
private boolean isImplementedRemoteServiceInterface(String intfName) {
synchronized (knownImplementedInterfaces) {
// See if it's cached.
//
if (knownImplementedInterfaces.contains(intfName)) {
return true;
}

// Unknown, so walk up the interface hierarchy and check each.
//
Class[] intfs = getClass().getInterfaces();
for (int i = 0; i < intfs.length; i++) {
Class intf = intfs[i];
if (isImplementedRemoteServiceInterfaceRecursive(intfName, intf)) {
knownImplementedInterfaces.add(intfName);
return true;
}
}
return false;
}
}

/**
* Only called from isImplementedInterface().
*/
private boolean isImplementedRemoteServiceInterfaceRecursive(String
intfName, Class intfToCheck) {
assert (intfToCheck.isInterface());

if (intfToCheck.getName().equals(intfName)) {
// The name is right, but we also verify that it is assignable to
// RemoteService.
//
if (RemoteService.class.isAssignableFrom(intfToCheck)) {
return true;
} else {
return false;
}
}

Class[] intfs = intfToCheck.getInterfaces();
for (int i = 0; i < intfs.length; i++) {
Class intf = intfs[i];
if (isImplementedRemoteServiceInterfaceRecursive(intfName, intf)) {
return true;
}
}

return false;
}

/**
* Gets the <code>HttpServletRequest</code> object for the current
call.
* It is stored thread-locally so that simultaneous invocations can
have
* different request objects.
*/
protected HttpServletRequest getThreadLocalRequest() {
return (HttpServletRequest) perThreadRequest.get();
}

/**
* Override this method to examine the serialized response that will
be
* returned to the client. The default implementation does nothing and
need
* not be called by subclasses.
*/
protected void onAfterResponseSerialized(String serializedResponse) {
}

/**
* Override this method to examine the serialized version of the
request
* payload before it is deserialized into objects. The default
* implementation does nothing and need not be called by subclasses.
*/
protected void onBeforeRequestDeserialized(String serializedRequest) {
}

/**
* Obtain the special package-prefixes we use to check for custom
* serializers that would like to live in a package that they cannot.
For
* example, "java.util.ArrayList" is in a sealed package, so instead
we use
* this prefix to check for a custom serializer in
* "com.google.gwt.user.client.rpc.core.java.util.ArrayList". Right
now,
* it's hard-coded because we don't have a pressing need for this
mechanism
* to be extensible, but it is imaginable, which is why it's
implemented
* this way.
*/
private String[] getPackagePaths() {
return new String[] { "com.google.gwt.user.client.rpc.core" };
}

/**
* Returns true if the {@link java.lang.reflect.Method Method}
definition on
* the service is specified to throw the exception contained in the
* InvocationTargetException or false otherwise.
*
* NOTE we do not check that the type is serializable here. We assume
that
* it must be otherwise the application would never have been allowed
to
* run.
*
* @param serviceIntfMethod
* @param e
* @return
*/
private boolean isExpectedException(Method serviceIntfMethod,
Throwable cause) {
assert (serviceIntfMethod != null);
assert (cause != null);

Class[] exceptionsThrown = serviceIntfMethod.getExceptionTypes();
if (exceptionsThrown.length <= 0) {
// The method is not specified to throw any exceptions
//
return false;
}

Class causeType = cause.getClass();

for (int index = 0; index < exceptionsThrown.length; ++index) {
Class exceptionThrown = exceptionsThrown[index];
assert (exceptionThrown != null);

if (exceptionThrown.isAssignableFrom(causeType)) {
return true;
}
}

return false;
}

private String readPayloadAsUtf8(HttpServletRequest request) throws
IOException, ServletException {
int contentLength = request.getContentLength();
if (contentLength == -1) {
// Content length must be known.
throw new ServletException("Content-Length must be specified");
}

String contentType = request.getContentType();
boolean contentTypeIsOkay = false;
// Content-Type must be specified.
if (contentType != null) {
// The type must be plain text.
if (contentType.startsWith("text/plain")) {
// And it must be UTF-8 encoded (or unspecified, in which case
// we assume
// that it's either UTF-8 or ASCII).
if (contentType.indexOf("charset=") == -1)
contentTypeIsOkay = true;
else if (contentType.indexOf("charset=utf-8") != -1)
contentTypeIsOkay = true;
}
}
if (!contentTypeIsOkay)
throw new ServletException(
"Content-Type must be 'text/plain' with 'charset=utf-8' (or
unspecified charset)");

InputStream in = request.getInputStream();
try {
byte[] payload = new byte[contentLength];
int offset = 0;
int len = contentLength;
int byteCount;
while (offset < contentLength) {
byteCount = in.read(payload, offset, len);
if (byteCount == -1)
throw new ServletException("Client did not send " + contentLength
+ " bytes as expected");
offset += byteCount;
len -= byteCount;
}
return new String(payload, "UTF-8");
} finally {
if (in != null) {
in.close();
}
}
}

/**
* Called when the machinery of this class itself has a problem,
rather than
* the invoked third-party method. It writes a simple 500 message back
to
* the client.
*/
private void respondWithFailure(HttpServletResponse response,
Throwable caught) {
ServletContext servletContext = getServletContext();
servletContext.log("Exception while dispatching incoming RPC call",
caught);
try {
response.setContentType("text/plain");
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
String msg = "The call failed on the server; see server log for
details";
response.getWriter().write(msg);
} catch (IOException e) {
servletContext.log("sendError() failed while sending the previous
failure to the client", caught);
}
}

private void writeResponse(HttpServletRequest request,
HttpServletResponse response, String responsePayload)
throws IOException {

byte[] reply = responsePayload.getBytes(CHARSET_UTF8);
String contentType = CONTENT_TYPE_TEXT_PLAIN_UTF8;
int contentLength;

if (shouldCompress(request, responsePayload)) {
// Compress the reply and adjust headers.
//
ByteArrayOutputStream output = null;
GZIPOutputStream gzipOutputStream = null;
Throwable caught = null;
try {
output = new ByteArrayOutputStream(reply.length);
gzipOutputStream = new GZIPOutputStream(output);
gzipOutputStream.write(reply);
gzipOutputStream.finish();
gzipOutputStream.flush();
response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP);
reply = output.toByteArray();
} catch (UnsupportedEncodingException e) {
caught = e;
} catch (IOException e) {
caught = e;
} finally {
if (null != gzipOutputStream) {
gzipOutputStream.close();
}
if (null != output) {
output.close();
}
}

if (caught != null) {
getServletContext().log("Unable to compress response", caught);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}

// Send the reply.
//
response.setContentLength(reply.length);
response.setContentType(contentType);
response.getOutputStream().write(reply);
response.setStatus(HttpServletResponse.SC_OK);
}

/**
* @param stream
* @param responseType
* @param responseObj
* @param isException
* @return
*/
private String createResponse(ServerSerializationStream stream, Class
responseType, Object responseObj,
boolean isException) {
stream.prepareToWrite();
stream.writeInt(SerializationStream.SERIALIZATION_STREAM_VERSION);
stream.writeInt(stream.getFlags());
if (responseType != void.class) {
try {
stream.serializeValue(responseObj, responseType);
} catch (SerializationException e) {
responseObj = e;
isException = true;
}
}

String bufferStr = (isException ? "{EX}" : "{OK}") +
stream.toString();
return bufferStr;
}

@Override
protected ModelAndView handleRequestInternal(HttpServletRequest
request, HttpServletResponse response)
throws Exception {
Throwable caught;
try {
// Store the request object in thread-local storage.
//
perThreadRequest.set(request);

// Read the request fully.
//
String requestPayload = readPayloadAsUtf8(request);

// Invoke the core dispatching logic, which returns the serialized
// result.
//
String responsePayload = processCall(requestPayload);

// Write the response.
//
writeResponse(request, response, responsePayload);

return null;

} catch (IOException e) {
caught = e;
} catch (ServletException e) {
caught = e;
} catch (SerializationException e) {
caught = e;
} catch (Throwable e) {
caught = e;
}

respondWithFailure(response, caught);
return null;
}

public abstract void init();

static {
TYPE_NAMES = new HashMap();
TYPE_NAMES.put("Z", boolean.class);
TYPE_NAMES.put("B", byte.class);
TYPE_NAMES.put("C", char.class);
TYPE_NAMES.put("D", double.class);
TYPE_NAMES.put("F", float.class);
TYPE_NAMES.put("I", int.class);
TYPE_NAMES.put("J", long.class);
TYPE_NAMES.put("S", short.class);
}

private final ThreadLocal perThreadRequest = new ThreadLocal();

private final ServerSerializableTypeOracle serializableTypeOracle;

private final Set knownImplementedInterfaces = new HashSet();

}
</code>

semiot...@gmail.com

unread,
Jul 11, 2006, 10:42:47 AM7/11/06
to Google Web Toolkit
How does one use this? How much has it been tested?

georgeuoa

unread,
Jul 12, 2006, 2:29:52 AM7/12/06
to Google Web Toolkit
Well, if you use Spring then it's quite easy:

<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">

<property name="mappings">
<props>
<prop key="*.query">MySpringGWTController</prop>
</props>
</property>
</bean>

<bean id="MySpringGWTController"
class="some.package.MySpringGWTController" init-method="init">
...
</bean>

And extend the Controller like:

public class MySpringGWTController extends GWTSpringController
implements
SomeQueryInterface {
...
}

Now, about Q&A, I am using it for my own application and have not run
into any problems so far, but otherwise, it's not tested.

georgeuoa

unread,
Jul 12, 2006, 6:51:11 AM7/12/06
to Google Web Toolkit
Darn, there is a much simpler and better solution, sorry for the
confusion:

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

/**
* Implementation of a GWT-Spring controller that forwards a request
made
* to the doPost method.
*/

public abstract class GWTSpringController extends RemoteServiceServlet
implements Controller {

public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
doPost(request, response);
return null;
}

}

semiot...@gmail.com

unread,
Jul 12, 2006, 4:07:41 PM7/12/06
to Google Web Toolkit
OK, now I think I understand what I'm looking at. However, I don't see
how this Spring integration brings you much except by mapping the URL
to the service through Spring and the DispatcherServlet instead of
going straight to tomcat. Well, I can see the advantage of this if you
already have a site that uses Spring and you want to add some GWT
features, and keep all that configuration in your application context
file. Correct me if I'm wrong, because I'm new to Spring (not to
mention GWT, but who's not?).

My other question is, where did this come from? There was mention of a
cut and paste but I'm wondering where the cut was from. Excuse my
ignorance if I'm jumping into a conversation-in-progress...that's the
feeling I'm getting.

Message has been deleted

Jean-Francois Briere

unread,
Jul 12, 2006, 10:50:19 PM7/12/06
to Google Web Toolkit
Thanks georgeuoa,

This is indeed a very clean and elegant solution.
You should start a new thread with this class as a contribution along a
simple example on how to integrate it in Spring.
Eg.:
1- The GWT RPC service implementation should extend this class.
2- In web.xml add the DispatcherServlet servlet
3- In xxx-servlet.xml use SimpleUrlHandlerMapping bean (for instance)
to do the mapping to the implemented GWT RPC service.

Great job!

Regards

Jean-Francois Briere

unread,
Jul 12, 2006, 11:03:17 PM7/12/06
to Google Web Toolkit
> I don't see how this Spring integration brings you much except by mapping the URL
> to the service through Spring and the DispatcherServlet instead of
> going straight to tomcat.

There are many things that your GWT RPC services can profit from the
Spring framework.
Easy transactional integration.
Easy persistence (Hibernate for instance) integration.
Easy security integration (with Acegi for instance).

In general: Easy "what-is-hard-and-non-protable-to-implement"
integration.

Regards

semiot...@gmail.com

unread,
Jul 13, 2006, 3:36:11 PM7/13/06
to Google Web Toolkit
Definitely true, but you don't need to do THIS to use Spring's other
features with GWT services. You can implement the GWT service
normally, and then simply load whatever application context you want
from there. Or so it seems to me.

semiot...@gmail.com

unread,
Jul 13, 2006, 3:56:57 PM7/13/06
to Google Web Toolkit
I see now, georgeuoa has explained it quite well on this new thread:

http://groups.google.com/group/Google-Web-Toolkit/browse_frm/thread/c87dacf13d878fb2/d1a938f39add9f96?hl=en#d1a938f39add9f96

It is not about using Spring from GWT services; it is the other way
around.

Reply all
Reply to author
Forward
0 new messages