org.gwtwidgets.server.spring.GWTRPCServiceExporter.java Source code

Java tutorial

Introduction

Here is the source code for org.gwtwidgets.server.spring.GWTRPCServiceExporter.java

Source

/*
 * 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.gwtwidgets.server.spring;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Map;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gwtwidgets.server.spring.util.ImmutableCopyMap;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.ServletConfigAware;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.ModelAndView;

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.RPCRequest;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.user.server.rpc.RPC;
import com.google.gwt.user.server.rpc.SerializationPolicy;

/**
 * This component publishes an object (see {@link #setService(Object)}) as a
 * service to the GWT RPC protocol. Service targets can be:<p/>
 * <ul>
 * <li>POJOs which don't have to extend any class or implement any interface.
 * However you should provide a service interface (see
 * {@link #setServiceInterfaces(Class[])})</li>
 * <li>POJOs which implement {@link RemoteService}</li>
 * <li>You can extend the GWTRPCServiceExporter which assigns the target
 * service to itself.</li>
 * </ul>
 * <p/>
 * Exceptions directly thrown from the target service are propagated back to the
 * client. For special exception handling you can override the various <code>handle</code>*
 * methods which are invoked by the GWTRPCServiceExporter.
 * 
 * @author George Georgovassilis, g.georgovassilis[at]gmail.com
 * @author Max Jonas Werner
 */
public class GWTRPCServiceExporter extends RemoteServiceServlet
        implements RPCServiceExporter, ServletContextAware, ServletConfigAware, BeanNameAware {

    /**
     * Disable RPC response compression. Value is 0.
     */
    public final static int COMPRESSION_DISABLED = 0;

    /**
     * Leave default RPC response compression behavior. Value is 1.
     */

    public final static int COMPRESSION_AUTO = 1;

    /**
     * Force compression of all RPC responses. Value is 2.
     */
    public final static int COMPRESSION_ENABLED = 2;

    protected Log logger = LogFactory.getLog(getClass());

    protected Class<?>[] serviceInterfaces;

    protected Object service = this;

    protected ServletContext servletContext;

    protected int compressResponse = COMPRESSION_AUTO;

    protected boolean disableResponseCaching = false;

    protected boolean throwUndeclaredExceptionToServletContainer = false;

    protected String beanName = "GWTRPCServiceExporter";

    protected SerializationPolicyProvider serializationPolicyProvider = new DefaultSerializationPolicyProvider();

    protected int serializationFlags = AbstractSerializationStream.DEFAULT_FLAGS;

    protected boolean shouldCheckPermutationStrongName = false;

    protected ModulePathTranslation modulePathTranslation = new ModulePathTranslation() {

        public String computeModuleBaseURL(HttpServletRequest request, String moduleBaseURL, String strongName) {
            return moduleBaseURL;
        }
    };

    public void setModulePathTranslation(ModulePathTranslation modulePathTranslation) {
        this.modulePathTranslation = modulePathTranslation;
    }

    @Override
    protected void checkPermutationStrongName() {
        if (shouldCheckPermutationStrongName)
            super.checkPermutationStrongName();
    }

    public void setShouldCheckPermutationStrongName(boolean shouldCheckPermutationStrongName) {
        this.shouldCheckPermutationStrongName = shouldCheckPermutationStrongName;
    }

    /**
     * Return the set serialization flags (see {@link AbstractSerializationStream#getFlags()}
     * @return
     */
    public int getSerializationFlags() {
        return serializationFlags;
    }

    /**
     * Set serialization flags (see {@link AbstractSerializationStream#getFlags()}. Default value is
     * {@link AbstractSerializationStream#DEFAULT_FLAGS}
     * @param serializationFlags
     */
    public void setSerializationFlags(int serializationFlags) {
        this.serializationFlags = serializationFlags;
    }

    /**
     * Post-processes an RPC response. Default method returns the method's argument
     * @param response Response
     * @return Possibly changed response
     */
    protected String processResponse(String response) {
        return response;
    }

    @Override
    protected void doUnexpectedFailure(Throwable e) {
        super.doUnexpectedFailure(e);
        if (throwUndeclaredExceptionToServletContainer)
            throw new RuntimeException(e);
    }

    /*
     * Concurrent put/get invocations are reasonably safe in this use case on an
     * ImmutableCopyMap.
     */
    protected Map<Method, Method> methodCache = new ImmutableCopyMap<Method, Method>();

    /**
     * Disables HTTP response caching by modifying response headers for browsers.
     * Can be overridden by extending classes to change behaviour.
     * @param request
     * @param response
     */
    protected void preprocessHTTP(HttpServletRequest request, HttpServletResponse response) {
        if (disableResponseCaching)
            ServletUtils.disableResponseCaching(response);
    }

    /**
     * Returns the installed serialization policy provider. If none other was specified,
     * the {@link DefaultSerializationPolicyProvider} is used
     * @return
     */
    public SerializationPolicyProvider getSerializationPolicyProvider() {
        return serializationPolicyProvider;
    }

    /**
     * Assign a new serialization policy provider.
     * @param serializationPolicyProvider
     */
    public void setSerializationPolicyProvider(SerializationPolicyProvider serializationPolicyProvider) {
        this.serializationPolicyProvider = serializationPolicyProvider;
    }

    /**
     * Implementation of {@link ServletContextAware}, is invoked by the Spring
     * application context.
     * 
     * @param servletContext
     */
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    /**
     * Returns the servlet context
     * @return {@link ServletContext}
     */
    @Override
    public ServletContext getServletContext() {
        return servletContext;
    }

    @Override
    protected void onAfterResponseSerialized(String serializedResponse) {
        if (logger.isTraceEnabled())
            logger.trace("Serialised RPC response: [" + serializedResponse + "]");
    }

    @Override
    protected void onBeforeRequestDeserialized(String serializedRequest) {
        if (logger.isTraceEnabled())
            logger.trace("Serialised RPC request: [" + serializedRequest + "]");
    }

    /**
     * Handles method invocation on a service and is invoked by
     * {@link #processCall(String)}.
     * 
     * @param service
     *            Service to invoke method on
     * @param targetMethod
     *            Method to invoke.
     * @param targetParameters
     *            Parameters to pass to method. Can be null for no arguments.
     * @param rpcRequest
     *            RPCRequest instance for this request
     * @return Return RPC encoded result.
     * @throws Exception
     */
    protected String invokeMethodOnService(Object service, Method targetMethod, Object[] targetParameters,
            RPCRequest rpcRequest) throws Exception {
        Object result = targetMethod.invoke(service, targetParameters);
        SerializationPolicy serializationPolicy = getSerializationPolicyProvider()
                .getSerializationPolicyForSuccess(rpcRequest, service, targetMethod, targetParameters, result);
        String encodedResult = RPC.encodeResponseForSuccess(rpcRequest.getMethod(), result, serializationPolicy,
                serializationFlags);
        return encodedResult;
    }

    /**
     * Handles an exception which is raised when a method invocation with bad
     * arguments is attempted. This implementation throws a
     * {@link SecurityException}. For details on arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e
     *            Exception thrown
     * @param service
     * @param targetMethod
     * @return RPC encoded response (such as an RPC client exception)
     */
    protected String handleIllegalArgumentException(IllegalArgumentException e, Object service, Method targetMethod,
            RPCRequest rpcRequest) {
        SecurityException securityException = new SecurityException(
                "Blocked attempt to invoke method " + targetMethod);
        securityException.initCause(e);
        throw securityException;
    }

    /**
     * Handles an exception which is raised when a method access is attempted to
     * a method which is not part of the RPC interface. This method is invoked
     * by {@link #processCall(String)}. This implementation throws a
     * {@link SecurityException}. For details on arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e
     *            Exception thrown
     * @param service
     * @param targetMethod
     * @return RPC encoded response (such as an RPC client exception)
     */
    protected String handleIllegalAccessException(IllegalAccessException e, Object service, Method targetMethod,
            RPCRequest rpcRequest) {
        SecurityException securityException = new SecurityException("Blocked attempt to access inaccessible method "
                + targetMethod + (service != null ? " on service " + service : ""));
        securityException.initCause(e);
        throw securityException;
    }

    /**
     * Wrapper around RPC utility invocation
     * @param rpcRequest RPCRequest
     * @param cause Exception to handle
     * @param targetMethod Method which threw the exception
     * @param targetParameters Method arguments  
     * @return RPC payload
     * @throws Exception
     */
    protected String encodeResponseForFailure(RPCRequest rpcRequest, Throwable cause, Method targetMethod,
            Object[] targetParameters) throws SerializationException {
        SerializationPolicy serializationPolicy = getSerializationPolicyProvider()
                .getSerializationPolicyForFailure(rpcRequest, service, targetMethod, targetParameters, cause);
        return RPC.encodeResponseForFailure(rpcRequest.getMethod(), cause, serializationPolicy, serializationFlags);
    }

    /**
     * Handles exceptions thrown by the target service, which are wrapped in
     * {@link InvocationTargetException}s due to invocation by reflection. This
     * method is invoked by {@link #processCall(String)}. This implementation
     * encodes exceptions as RPC errors and returns them. For details on
     * arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e
     *            Exception thrown
     * @param service
     * @param targetMethod
     * @param parameters
     * @param rpcRequest
     * @return RPC payload
     * @throws Exception
     */
    protected String handleInvocationTargetException(InvocationTargetException e, Object service,
            Method targetMethod, Object[] parameters, RPCRequest rpcRequest) throws Exception {
        Throwable cause = e.getCause();
        if (!(cause instanceof RuntimeException))
            logger.warn(cause);
        return encodeResponseForFailure(rpcRequest, cause, targetMethod, parameters);
    }

    /**
     * Handles exceptions thrown during a service invocation that are not
     * handled by other exception handlers. {@link #processCall(String)} on
     * exceptions which have escaped the other exception handlers such as
     * {@link #handleIllegalAccessException(IllegalAccessException, Object, Method, RPCRequest)}
     * etc. This implementation re-casts 'e'. For details on arguments please
     * consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e
     *            Exception thrown
     * @param service
     * @param targetMethod
     * @param rpcRequest
     * @return RPC payload
     * @throws Exception
     */
    protected String handleServiceException(Exception e, Object service, Method targetMethod, RPCRequest rpcRequest)
            throws Exception {
        throw e;
    }

    /**
     * Handles {@link UndeclaredThrowableException}s which are thrown by the
     * target service. This method is invoked by
     * {@link #processCall(String)}. This implementation re-casts 'e'. For
     * details on arguments please consult
     * {@link #invokeMethodOnService(Object, Method, Object[], RPCRequest)}.
     * 
     * @param e
     *            Exception thrown
     * @param service
     * @param targetMethod
     * @param rpcRequest
     * @return RPC payload
     * @throws Exception
     */
    protected String handleUndeclaredThrowableException(Exception e, Object service, Method targetMethod,
            RPCRequest rpcRequest) throws Exception {
        throw e;
    }

    /**
     * Returns method to invoke on service. This implementation calls
     * {@link ReflectionUtils#getRPCMethod(Object, Class[], Method)}
     * 
     * @param decodedMethod
     *            Method as determined by RPC
     * @return Method to invoke.
     * @throws NoSuchMethodException
     */
    protected Method getMethodToInvoke(Method decodedMethod) throws NoSuchMethodException {
        // Synchronization is unnecessary here, the worst thing that can happen
        // is that a method key is put multiple times in a map, which still
        // would leave only the latest addition. After a short time of operation
        // the method cache should get no writes at all, which makes the
        // immutable copy pattern a good choice.
        Method method = methodCache.get(decodedMethod);
        if (method != null)
            return method;
        method = ReflectionUtils.getRPCMethod(service, serviceInterfaces, decodedMethod);
        return methodCache.put(decodedMethod, method);
    }

    /**
     * Overridden from {@link RemoteServiceServlet} and invoked by the servlet
     * code.
     */
    @Override
    public String processCall(String payload) throws SerializationException {
        try {
            checkPermutationStrongName();
            // Copy & pasted & edited from the GWT 1.4.3 RPC documentation
            RPCRequest rpcRequest = RPC.decodeRequest(payload, null, this);
            onAfterRequestDeserialized(rpcRequest);
            Method targetMethod = getMethodToInvoke(rpcRequest.getMethod());
            Object[] targetParameters = rpcRequest.getParameters();

            try {
                return processResponse(invokeMethodOnService(service, targetMethod, targetParameters, rpcRequest));
            } catch (IllegalArgumentException e) {
                return processResponse(handleIllegalArgumentException(e, service, targetMethod, rpcRequest));
            } catch (IllegalAccessException e) {
                return processResponse(handleIllegalAccessException(e, service, targetMethod, rpcRequest));
            } catch (InvocationTargetException e) {
                return processResponse(
                        handleInvocationTargetException(e, service, targetMethod, targetParameters, rpcRequest));
            } catch (UndeclaredThrowableException e) {
                return processResponse(handleUndeclaredThrowableException(e, service, targetMethod, rpcRequest));
            } catch (Exception e) {
                return processResponse(handleServiceException(e, service, targetMethod, rpcRequest));
            }
        } catch (IncompatibleRemoteServiceException e) {
            return processResponse(handleIncompatibleRemoteServiceException(e));
        } catch (Exception e) {
            return processResponse(handleExporterProcessingException(e));
        }
    }

    /**
     * Invoked by {@link #processCall(String)} when RPC throws an
     * {@link IncompatibleRemoteServiceException}. This implementation
     * propagates the exception back to the client via RPC.
     * 
     * @param e
     *            Exception thrown
     * @return RPC encoded failure response
     * @throws SerializationException
     */
    protected String handleIncompatibleRemoteServiceException(IncompatibleRemoteServiceException cause)
            throws SerializationException {
        logger.warn(cause.getMessage());
        return RPC.encodeResponseForFailure(null, cause);
    }

    /**
     * Invoked by {@link #processCall(String)} for an exception if no suitable
     * exception handler was found. This is the outermost exception handler,
     * catching any exceptions not caught by other exception handlers or even
     * thrown by those handlers. This implementation wraps 'e' in a
     * {@link RuntimeException} which is then thrown.
     * 
     * @param e
     * @return RPC encoded failure response
     */
    protected String handleExporterProcessingException(Exception e) {
        throw new RuntimeException(e);
    }

    /**
     * Set the wrapped service bean. RPC requests are decoded and the corresponding
     * method of the service object is invoked.
     * 
     * @param service Service to which the decoded requests are forwarded
     */
    public void setService(Object service) {
        this.service = service;
    }

    /**
     * Implementation of inherited interface
     * @see {@link HttpRequestHandler#handleRequest(HttpServletRequest, HttpServletResponse)}
     */
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            preprocessHTTP(request, response);
            ServletUtils.setResponse(response);
            doPost(request, response);
            return null;
        } finally {
            ServletUtils.setResponse(null);
        }
    }

    /**
     * Specifies the interfaces which must be implemented by the service bean.
     * If not specified then any interface extending {@link RemoteService} which
     * is implemented by the service bean is assumed. Implementation note:
     * as methods are only lazily bound to the service implementation you may get
     * away with mismatches between the specified interfaces and the actual implementation
     * as long as no method is invoked which has a different/missing signature in the interface
     * and the service implementation.
     * 
     * @param serviceInterfaces
     */
    public void setServiceInterfaces(Class<RemoteService>[] serviceInterfaces) {
        this.serviceInterfaces = serviceInterfaces;
    }

    /**
     * Should be invoked after all properties have been set. Normally invoked
     * by the Spring application context setup.
     * @see InitializingBean#afterPropertiesSet()
     */
    public void afterPropertiesSet() throws Exception {
        if (service == null)
            throw new Exception("You must specify a service object.");
        if (serviceInterfaces == null) {
            logger.debug("Discovering service interfaces");
            serviceInterfaces = ReflectionUtils.getExposedInterfaces(service.getClass());
            if (serviceInterfaces.length == 0)
                logger.warn("The specified service does neither implement RemoteService "
                        + "nor were any service interfaces specified. RPC access to *all* object methods is allowed.");
        }
        if (servletContext == null) {
            logger.warn(
                    "No servlet context found. You should declare a GWTRPCServiceExporter or GWTHandler in a servlet context and not the application context.");
        }
    }

    /**
     * Return target service. Each {@link GWTRPCServiceExporter} has a single
     * target service which it redirects RPC to.
     * 
     * @return Object
     */
    public Object getService() {
        return service;
    }

    @Override
    protected boolean shouldCompressResponse(HttpServletRequest request, HttpServletResponse response,
            String responsePayload) {
        switch (compressResponse) {
        case COMPRESSION_DISABLED:
            return false;
        case COMPRESSION_ENABLED:
            return true;
        }
        return super.shouldCompressResponse(request, response, responsePayload);
    }

    /**
     * Enables or disables compression of RPC output. Defaults to
     * {@link #COMPRESSION_AUTO}. Allowed values are
     * {@link #COMPRESSION_ENABLED}, {@link #COMPRESSION_DISABLED} and
     * {@link #COMPRESSION_AUTO}.
     * 
     * @param compressResponse
     */
    protected void setCompressResponse(int compressResponse) {
        if (compressResponse != COMPRESSION_ENABLED && compressResponse != COMPRESSION_DISABLED
                && compressResponse != COMPRESSION_AUTO)
            throw new IllegalArgumentException("Invalid compressResponse argumnet " + compressResponse);
        this.compressResponse = compressResponse;
    }

    /**
     * Can be used to set HTTP response headers that explicitly disable caching on the browser side.
     * Note that due to the additional headers the response size increases.
     * @param responseCaching
     */
    public void setResponseCachingDisabled(boolean disableResponseCaching) {
        this.disableResponseCaching = disableResponseCaching;
    }

    /**
     * When enabled will throw exceptions which originate from the service and have not been
     * declared in the RPC interface back to the servlet container.
     * @param throwUndeclaredExceptionToServletContainer Defaults to <code>false</code> 
     */
    public void setThrowUndeclaredExceptionToServletContainer(boolean throwUndeclaredExceptionToServletContainer) {
        this.throwUndeclaredExceptionToServletContainer = throwUndeclaredExceptionToServletContainer;
    }

    /**
     * Setter for servlet configuration
     */
    public void setServletConfig(ServletConfig servletConfig) {
        try {
            init(servletConfig);
        } catch (ServletException e) {
            throw new RuntimeException(e);
        }
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

    @Override
    protected SerializationPolicy doGetSerializationPolicy(HttpServletRequest request, String moduleBaseURL,
            String strongName) {
        String newModuleBaseURL = modulePathTranslation.computeModuleBaseURL(request, moduleBaseURL, strongName);
        return super.doGetSerializationPolicy(request, newModuleBaseURL, strongName);
    }

    @Override
    public String toString() {
        return beanName;
    }

}