/*
* Copyright 2004-2010 the Seasar Foundation and the Others.
*
* 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.
*/
package org.slim3.gwt.server.rpc;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slim3.controller.HotReloadingClassLoader;
import org.slim3.util.CipherFactory;
import org.slim3.util.ClassUtil;
import org.slim3.util.RequestLocator;
import org.slim3.util.ResponseLocator;
import org.slim3.util.ServletContextLocator;
import org.slim3.util.StringUtil;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.impl.AbstractSerializationStream;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.user.server.rpc.SerializationPolicy;
import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader;
import com.google.gwt.user.server.rpc.impl.TypeNameObfuscator;
/**
* The remote service servlet for Slim3.
*
* @author higa
* @since 1.0.0
*
*/
public class GWTServiceServlet extends RemoteServiceServlet {
private static final long serialVersionUID = 1L;
private static final HashMap<String, Class<?>> TYPE_NAMES;
static {
TYPE_NAMES = new HashMap<String, Class<?>>();
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);
}
/**
* Whether the servlet context is set to {@link ServletContextLocator}.
*/
protected boolean servletContextSet = false;
private static SerializationPolicy loadHotSerializationPolicy(
HttpServlet servlet, HttpServletRequest request,
String moduleBaseURL, String strongName) {
// The request can tell you the path of the web app relative to the
// container root.
String contextPath = request.getContextPath();
String modulePath = null;
if (moduleBaseURL != null) {
try {
modulePath = new URL(moduleBaseURL).getPath();
} catch (MalformedURLException ex) {
// log the information, we will default
servlet.log("Malformed moduleBaseURL: " + moduleBaseURL, ex);
}
}
SerializationPolicy serializationPolicy = null;
/*
* Check that the module path must be in the same web app as the servlet
* itself. If you need to implement a scheme different than this,
* override this method.
*/
if (modulePath == null || !modulePath.startsWith(contextPath)) {
String message =
"ERROR: The module path requested, "
+ modulePath
+ ", is not in the same web application as this servlet, "
+ contextPath
+ ". Your module may not be properly configured or your client and server code maybe out of date.";
servlet.log(message, null);
} else {
// Strip off the context path from the module base URL. It should be
// a
// strict prefix.
String contextRelativePath =
modulePath.substring(contextPath.length());
String serializationPolicyFilePath =
HotSerializationPolicyLoader
.getSerializationPolicyFileName(contextRelativePath
+ strongName);
// Open the RPC resource file and read its contents.
InputStream is =
servlet.getServletContext().getResourceAsStream(
serializationPolicyFilePath);
try {
if (is != null) {
try {
serializationPolicy =
HotSerializationPolicyLoader.loadFromStream(
is,
null);
} catch (ParseException e) {
servlet.log("ERROR: Failed to parse the policy file '"
+ serializationPolicyFilePath
+ "'", e);
} catch (IOException e) {
servlet.log("ERROR: Could not read the policy file '"
+ serializationPolicyFilePath
+ "'", e);
}
} else {
String message =
"ERROR: The serialization policy file '"
+ serializationPolicyFilePath
+ "' was not found; did you forget to include it in this deployment?";
servlet.log(message, null);
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignore this error
}
}
}
}
return serializationPolicy;
}
@Override
public void init() throws ServletException {
super.init();
getServletContext().setAttribute(
"slim3.controllerPackage",
"server.controller");
if (ServletContextLocator.get() == null) {
ServletContextLocator.set(getServletContext());
servletContextSet = true;
}
}
@Override
public void destroy() {
if (servletContextSet) {
ServletContextLocator.set(null);
}
}
/**
* Process a call originating from the given request. Uses the
* {@link RPC#invokeAndEncodeResponse(Object, java.lang.reflect.Method, Object[])}
* method to do the actual work.
* <p>
* Subclasses may optionally override this method to handle the payload in
* any way they desire (by routing the request to a framework component, for
* instance). The {@link HttpServletRequest} and {@link HttpServletResponse}
* can be accessed via the {@link #getThreadLocalRequest()} and
* {@link #getThreadLocalResponse()} methods.
* </p>
* This is public so that it can be unit tested easily without HTTP.
*
* @param payload
* the UTF-8 request payload
* @return a string which encodes either the method's return, an exception
* thrown by the method
* @throws SerializationException
* if we cannot serialize the response
*/
@Override
public String processCall(String payload) throws SerializationException {
HttpServletRequest previousRequest = RequestLocator.get();
if (previousRequest == null) {
RequestLocator.set(getThreadLocalRequest());
ResponseLocator.set(getThreadLocalResponse());
}
S3RPCRequest request = null;
RPCRequest rpcRequest = null;
try {
CipherFactory.getFactory().clearLimitedKey();
request = decodeRequest(payload);
rpcRequest = request.getOriginalRequest();
onAfterRequestDeserialized(rpcRequest);
Object result =
invoke(request.getService(), rpcRequest.getMethod(), rpcRequest
.getParameters());
return RPC.encodeResponseForSuccess(
rpcRequest.getMethod(),
result,
rpcRequest.getSerializationPolicy());
} catch (IncompatibleRemoteServiceException ex) {
log(
"An IncompatibleRemoteServiceException was thrown while processing this call.",
ex);
return RPC.encodeResponseForFailure(null, ex);
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
log("An exception was thrown while processing this call.", cause);
SerializationPolicy sp =
rpcRequest != null ? rpcRequest.getSerializationPolicy() : null;
return RPC.encodeResponseForFailure(null, cause, sp);
} finally {
if (previousRequest == null) {
RequestLocator.set(null);
ResponseLocator.set(null);
}
}
}
/**
* Returns RPC request.
*
* @param encodedRequest
* a string that encodes the service interface, the service
* method, and the arguments
* @return RPC request
*
* @throws NullPointerException
* if the encodedRequest is <code>null</code>
* @throws IllegalArgumentException
* if the encodedRequest is an empty string
* @throws IncompatibleRemoteServiceException
* if any of the following conditions apply:
* <ul>
* <li>if the types in the encoded request cannot be
* deserialized</li>
* <li>if the {@link ClassLoader} acquired from
* <code>Thread.currentThread().getContextClassLoader()</code>
* cannot load the service interface or any of the types
* specified in the encodedRequest</li>
* <li>the requested interface is not assignable to
* {@link RemoteService}</li>
* <li>the service method requested in the encodedRequest is not
* a member of the requested service interface</li>
* <li>the type parameter is not <code>null</code> and is not
* assignable to the requested {@link RemoteService} interface
* </ul>
*/
protected S3RPCRequest decodeRequest(String encodedRequest) {
if (encodedRequest == null) {
throw new NullPointerException(
"The encodedRequest parameter cannot be null.");
}
if (encodedRequest.length() == 0) {
throw new IllegalArgumentException(
"The encodedRequest parameter cannot be empty.");
}
ClassLoader classLoader =
Thread.currentThread().getContextClassLoader();
try {
ServerSerializationStreamReader streamReader =
new ServerSerializationStreamReader(classLoader, this);
streamReader.prepareToRead(encodedRequest);
String interfaceName = readClassName(streamReader);
Class<?> serviceClass = getServiceClass(interfaceName);
Object service = getService(serviceClass);
SerializationPolicy serializationPolicy =
streamReader.getSerializationPolicy();
String methodName = streamReader.readString();
int paramCount = streamReader.readInt();
Class<?>[] parameterTypes = new Class[paramCount];
for (int i = 0; i < parameterTypes.length; i++) {
String paramClassName = readClassName(streamReader);
parameterTypes[i] = getClass(paramClassName);
}
try {
Method method =
serviceClass.getMethod(methodName, parameterTypes);
Object[] parameterValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterValues.length; i++) {
parameterValues[i] =
streamReader.deserializeValue(parameterTypes[i]);
}
return new S3RPCRequest(service, new RPCRequest(
method,
parameterValues,
serializationPolicy,
streamReader.getFlags()));
} catch (NoSuchMethodException e) {
throw new IncompatibleRemoteServiceException(e.getMessage(), e);
}
} catch (SerializationException ex) {
throw new IncompatibleRemoteServiceException(ex.getMessage(), ex);
}
}
/**
* Returns class name.
*
* @param streamReader
* the stream reader
* @return class name
* @throws SerializationException
* if {@link SerializationException} occurred
*/
protected String readClassName(ServerSerializationStreamReader streamReader)
throws SerializationException {
String name = streamReader.readString();
int index;
if (streamReader
.hasFlags(AbstractSerializationStream.FLAG_ELIDE_TYPE_NAMES)) {
SerializationPolicy serializationPolicy =
streamReader.getSerializationPolicy();
if (!(serializationPolicy instanceof TypeNameObfuscator)) {
throw new IncompatibleRemoteServiceException(
"RPC request was encoded with obfuscated type names, "
+ "but the SerializationPolicy in use does not implement "
+ TypeNameObfuscator.class.getName());
}
String maybe =
((TypeNameObfuscator) serializationPolicy)
.getClassNameForTypeId(name);
if (maybe != null) {
return maybe;
}
} else if ((index = name.indexOf('/')) != -1) {
return name.substring(0, index);
}
return name;
}
/**
* Invokes the service method.
*
* @param service
* the service
* @param method
* the method
* @param args
* the arguments
* @return the return value of the method
* @throws InvocationTargetException
* if the method throws an exception
*/
protected Object invoke(Object service, Method method, Object[] args)
throws InvocationTargetException {
Object result = null;
try {
result = method.invoke(service, args);
} catch (IllegalArgumentException e) {
throw new IncompatibleRemoteServiceException(e.getMessage(), e);
} catch (IllegalAccessException e) {
throw new IncompatibleRemoteServiceException(e.getMessage(), e);
} catch (ClassCastException e) {
String msg = e.getMessage();
String[] msgs = StringUtil.split(msg, " ");
if (msgs.length > 2 && msgs[0].equals(msgs[msgs.length - 1])) {
msg =
"The class("
+ msgs[0]
+ ") is loaded by deferent class loaders.";
}
throw new IncompatibleRemoteServiceException(msg, e);
}
return result;
}
@Override
protected SerializationPolicy doGetSerializationPolicy(
HttpServletRequest request, String moduleBaseURL, String strongName) {
if (Thread.currentThread().getContextClassLoader() instanceof HotReloadingClassLoader) {
return loadHotSerializationPolicy(
this,
request,
moduleBaseURL,
strongName);
}
return super.doGetSerializationPolicy(
request,
moduleBaseURL,
strongName);
}
/**
* Returns the class specified by the name.
*
* @param className
* the class name
* @return the class
* @throws NullPointerException
* if the className parameter is null
*/
protected Class<?> getClass(String className) throws NullPointerException {
if (className == null) {
throw new NullPointerException("The className parameter is null.");
}
Class<?> clazz = TYPE_NAMES.get(className);
if (clazz != null) {
return clazz;
}
try {
return Class.forName(className, false, Thread
.currentThread()
.getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new IncompatibleRemoteServiceException(
"Could not load the class("
+ className
+ ") in context classloader",
e);
}
}
/**
* Returns the service class specified by the interface name.
*
* @param interfaceName
* the interface name
* @return the service class
* @throws NullPointerException
* if the interfaceName parameter is null
*/
protected Class<?> getServiceClass(String interfaceName)
throws NullPointerException {
if (interfaceName == null) {
throw new NullPointerException(
"The interfaceName parameter is null.");
}
String className =
interfaceName.replace(".client.", ".server.") + "Impl";
return getClass(className);
}
/**
* Returns the service.
*
* @param serviceClass
* the class name
* @return the service
* @throws NullPointerException
* if the serviceClass parameter is null
*/
protected Object getService(Class<?> serviceClass)
throws NullPointerException {
if (serviceClass == null) {
throw new NullPointerException(
"The serviceClass parameter is null.");
}
return ClassUtil.newInstance(serviceClass);
}
}