com.googlecode.jsonrpc4j.JsonRpcServer.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.jsonrpc4j.JsonRpcServer.java

Source

/*
The MIT License (MIT)
    
Copyright (c) 2014 jsonrpc4j
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */

package com.googlecode.jsonrpc4j;

import static com.googlecode.jsonrpc4j.ReflectionUtil.findMethods;
import static com.googlecode.jsonrpc4j.ReflectionUtil.getParameterAnnotations;
import static com.googlecode.jsonrpc4j.ReflectionUtil.getParameterTypes;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.googlecode.jsonrpc4j.ErrorResolver.JsonError;

/**
 * A JSON-RPC request server reads JSON-RPC requests from an
 * input stream and writes responses to an output stream.
 */
public class JsonRpcServer {

    private static final Logger LOGGER = Logger.getLogger(JsonRpcServer.class.getName());

    public static final String JSONRPC_RESPONSE_CONTENT_TYPE = "application/json-rpc";

    public static final ErrorResolver DEFAULT_ERRROR_RESOLVER = new MultipleErrorResolver(
            AnnotationsErrorResolver.INSTANCE, DefaultErrorResolver.INSTANCE);

    private static Class<?> WEBPARAM_ANNOTATION_CLASS;
    private static Method WEBPARAM_NAME_METHOD;

    private boolean backwardsComaptible = true;
    private boolean rethrowExceptions = false;
    private boolean allowExtraParams = false;
    private boolean allowLessParams = false;
    private ErrorResolver errorResolver = null;
    private ObjectMapper mapper;
    private Object handler;
    private Class<?> remoteInterface;
    private InvocationListener invocationListener = null;
    private Level exceptionLogLevel = Level.WARNING;

    static {
        ClassLoader classLoader = JsonRpcServer.class.getClassLoader();
        try {
            WEBPARAM_ANNOTATION_CLASS = classLoader.loadClass("javax.jws.WebParam");
            WEBPARAM_NAME_METHOD = WEBPARAM_ANNOTATION_CLASS.getMethod("name");
        } catch (Exception e) {
            // Must be Java 1.5
        }
    }

    /**
     * Creates the server with the given {@link ObjectMapper} delegating
     * all calls to the given {@code handler} {@link Object} but only
     * methods available on the {@code remoteInterface}.
     *
     * @param mapper the {@link ObjectMapper}
     * @param handler the {@code handler}
     * @param remoteInterface the interface
     */
    public JsonRpcServer(ObjectMapper mapper, Object handler, Class<?> remoteInterface) {
        this.mapper = mapper;
        this.handler = handler;
        this.remoteInterface = remoteInterface;
    }

    /**
     * Creates the server with the given {@link ObjectMapper} delegating
     * all calls to the given {@code handler}.
     *
     * @param mapper the {@link ObjectMapper}
     * @param handler the {@code handler}
     */
    public JsonRpcServer(ObjectMapper mapper, Object handler) {
        this(mapper, handler, null);
    }

    /**
     * Creates the server with a default {@link ObjectMapper} delegating
     * all calls to the given {@code handler} {@link Object} but only
     * methods available on the {@code remoteInterface}.
     *
     * @param handler the {@code handler}
     * @param remoteInterface the interface
     */
    public JsonRpcServer(Object handler, Class<?> remoteInterface) {
        this(new ObjectMapper(), handler, null);
    }

    /**
     * Creates the server with a default {@link ObjectMapper} delegating
     * all calls to the given {@code handler}.
     *
     * @param handler the {@code handler}
     */
    public JsonRpcServer(Object handler) {
        this(new ObjectMapper(), handler, null);
    }

    /**
     * Handles a portlet request.
     *
     * @param request the {@link ResourceRequest}
     * @param response the {@link ResourceResponse}
     * @throws IOException on error
     */
    public void handle(ResourceRequest request, ResourceResponse response) throws IOException {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Handing ResourceRequest " + request.getMethod());
        }

        // set response type
        response.setContentType(JSONRPC_RESPONSE_CONTENT_TYPE);

        // setup streams
        InputStream input = null;
        OutputStream output = response.getPortletOutputStream();

        // POST
        if (request.getMethod().equals("POST")) {
            input = request.getPortletInputStream();

            // GET
        } else if (request.getMethod().equals("GET")) {
            input = createInputStream(request.getParameter("method"), request.getParameter("id"),
                    request.getParameter("params"));

            // invalid request
        } else {
            throw new IOException("Invalid request method, only POST and GET is supported");
        }

        // service the request
        handle(input, output);
    }

    /**
     * Handles a servlet request.
     *
     * @param request the {@link HttpServletRequest}
     * @param response the {@link HttpServletResponse}
     * @throws IOException on error
     */
    public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Handing HttpServletRequest " + request.getMethod());
        }

        // set response type
        response.setContentType(JSONRPC_RESPONSE_CONTENT_TYPE);

        // setup streams
        InputStream input = null;
        OutputStream output = response.getOutputStream();

        // POST
        if (request.getMethod().equals("POST")) {
            input = request.getInputStream();

            // GET
        } else if (request.getMethod().equals("GET")) {
            input = createInputStream(request.getParameter("method"), request.getParameter("id"),
                    request.getParameter("params"));

            // invalid request
        } else {
            throw new IOException("Invalid request method, only POST and GET is supported");
        }

        // service the request
        handle(input, output);
    }

    /**
     * Handles a single request from the given {@link InputStream},
     * that is to say that a single {@link JsonNode} is read from
     * the stream and treated as a JSON-RPC request.  All responses
     * are written to the given {@link OutputStream}.
     *
     * @param ips the {@link InputStream}
     * @param ops the {@link OutputStream}
     * @throws IOException on error
     */
    public void handle(InputStream ips, OutputStream ops) throws IOException {

        // get node iterator
        ReadContext ctx = ReadContext.getReadContext(ips, mapper);

        // prcess
        JsonNode jsonNode = null;
        try {
            ctx.assertReadable();
            jsonNode = ctx.nextValue();
        } catch (JsonParseException e) {
            writeAndFlushValue(ops, createErrorResponse("jsonrpc", "null", -32700, "Parse error", null));
            return;
        }
        handleNode(jsonNode, ops);
    }

    /**
     * Returns parameters into an {@link InputStream} of JSON data.
     *
     * @param method the method
     * @param id the id
     * @param params the base64 encoded params
     * @return the {@link InputStream}
     * @throws IOException on error
     */
    protected static InputStream createInputStream(String method, String id, String params) throws IOException {

        // decode parameters
        String decodedParams = URLDecoder.decode(new String(Base64.decode(params)), "UTF-8");

        // create request
        String request = new StringBuilder().append("{ ").append("\"id\": \"").append(id).append("\", ")
                .append("\"method\": \"").append(method).append("\", ").append("\"params\": ").append(decodedParams)
                .append(" ").append("}").toString();

        // turn into InputStream
        return new ByteArrayInputStream(request.getBytes());
    }

    /**
     * Returns the handler's class or interfaces.  The variable serviceName
     * is ignored in this class.
     *
     * @param serviceName the optional name of a service
     * @return the class
     */
    protected Class<?>[] getHandlerInterfaces(String serviceName) {
        if (remoteInterface != null) {
            return new Class<?>[] { remoteInterface };
        } else if (Proxy.isProxyClass(handler.getClass())) {
            return handler.getClass().getInterfaces();
        } else {
            return new Class<?>[] { handler.getClass() };
        }
    }

    /**
     * Handles the given {@link JsonNode} and writes the
     * responses to the given {@link OutputStream}.
     *
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws IOException on error
     */
    public void handleNode(JsonNode node, OutputStream ops) throws IOException {

        // handle objects
        if (node.isObject()) {
            handleObject(ObjectNode.class.cast(node), ops);

            // handle arrays
        } else if (node.isArray()) {
            handleArray(ArrayNode.class.cast(node), ops);

            // bail on bad data
        } else {
            this.writeAndFlushValue(ops, this.createErrorResponse("2.0", "null", -32600, "Invalid Request", null));
        }
    }

    /**
     * Handles the given {@link ArrayNode} and writes the
     * responses to the given {@link OutputStream}.
     *
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws IOException on error
     */
    public void handleArray(ArrayNode node, OutputStream ops) throws IOException {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Handing " + node.size() + " requests");
        }

        // loop through each array element
        ops.write('[');
        for (int i = 0; i < node.size(); i++) {
            handleNode(node.get(i), ops);
            if (i != node.size() - 1)
                ops.write(',');
        }
        ops.write(']');
    }

    /**
     * Handles the given {@link ObjectNode} and writes the
     * responses to the given {@link OutputStream}.
     *
     * @param node the {@link JsonNode}
     * @param ops the {@link OutputStream}
     * @throws IOException on error
     */
    public void handleObject(ObjectNode node, OutputStream ops) throws IOException {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Request: " + node.toString());
        }

        // validate request
        if (!backwardsComaptible && !node.has("jsonrpc") || !node.has("method")) {
            writeAndFlushValue(ops, createErrorResponse("2.0", "null", -32600, "Invalid Request", null));
            return;
        }

        // get nodes
        JsonNode jsonPrcNode = node.get("jsonrpc");
        JsonNode methodNode = node.get("method");
        JsonNode idNode = node.get("id");
        JsonNode paramsNode = node.get("params");

        // get node values
        String jsonRpc = (jsonPrcNode != null && !jsonPrcNode.isNull()) ? jsonPrcNode.asText() : "2.0";
        String methodName = getMethodName(methodNode);
        String serviceName = getServiceName(methodNode);
        Object id = parseId(idNode);

        // find methods
        Set<Method> methods = new HashSet<Method>();
        methods.addAll(findMethods(getHandlerInterfaces(serviceName), methodName));
        if (methods.isEmpty()) {
            writeAndFlushValue(ops, createErrorResponse(jsonRpc, id, -32601, "Method not found", null));
            return;
        }

        // choose a method
        MethodAndArgs methodArgs = findBestMethodByParamsNode(methods, paramsNode);
        if (methodArgs == null) {
            writeAndFlushValue(ops, createErrorResponse(jsonRpc, id, -32602, "Invalid method parameters", null));
            return;
        }

        // invoke the method
        JsonNode result = null;
        Throwable thrown = null;
        long beforeMs = System.currentTimeMillis();

        if (invocationListener != null) {
            invocationListener.willInvoke(methodArgs.method, methodArgs.arguments);
        }

        try {
            result = invoke(getHandler(serviceName), methodArgs.method, methodArgs.arguments);
        } catch (Throwable e) {
            thrown = e;
        }

        if (invocationListener != null) {
            invocationListener.didInvoke(methodArgs.method, methodArgs.arguments, result, thrown,
                    System.currentTimeMillis() - beforeMs);
        }

        // respond if it's not a notification request
        if (id != null) {

            // attempt to resolve the error
            JsonError error = null;
            if (thrown != null) {

                // get cause of exception
                Throwable e = thrown;
                if (InvocationTargetException.class.isInstance(e)) {
                    e = InvocationTargetException.class.cast(e).getTargetException();
                }

                // resolve error
                if (errorResolver != null) {
                    error = errorResolver.resolveError(e, methodArgs.method, methodArgs.arguments);
                } else {
                    error = DEFAULT_ERRROR_RESOLVER.resolveError(e, methodArgs.method, methodArgs.arguments);
                }

                // make sure we have a JsonError
                if (error == null) {
                    error = new JsonError(0, e.getMessage(), e.getClass().getName());
                }
            }

            // the resoponse object
            ObjectNode response = null;

            // build error
            if (error != null) {
                response = createErrorResponse(jsonRpc, id, error.getCode(), error.getMessage(), error.getData());

                // build success
            } else {
                response = createSuccessResponse(jsonRpc, id, result);
            }

            // write it
            writeAndFlushValue(ops, response);
        }

        // log and potentially re-throw errors
        if (thrown != null) {
            if (LOGGER.isLoggable(exceptionLogLevel)) {
                LOGGER.log(exceptionLogLevel, "Error in JSON-RPC Service", thrown);
            }
            if (rethrowExceptions) {
                throw new RuntimeException(thrown);
            }
        }
    }

    /**
     * Get the service name from the methodNode.  In this class, it is always
     * <code>null</code>.  Subclasses may parse the methodNode for service name.
     *
     * @param methodNode the JsonNode for the method
     * @return the name of the service, or <code>null</code>
     */
    protected String getServiceName(JsonNode methodNode) {
        return null;
    }

    /**
     * Get the method name from the methodNode.
     *
     * @param methodNode the JsonNode for the method
     * @return the name of the method that should be invoked
     */
    protected String getMethodName(JsonNode methodNode) {
        return (methodNode != null && !methodNode.isNull()) ? methodNode.asText() : null;
    }

    /**
     * Get the handler (object) that should be invoked to execute the specified
     * RPC method.  Used by subclasses to return handlers specific to a service.
     *
     * @param serviceName an optional service name
     * @return the handler to invoke the RPC call against
     */
    protected Object getHandler(String serviceName) {
        return handler;
    }

    /**
     * Invokes the given method on the {@code handler} passing
     * the given params (after converting them to beans\objects)
     * to it.
     *
     * @param an optional service name used to locate the target object
     *  to invoke the Method on
     * @param m the method to invoke
     * @param params the params to pass to the method
     * @return the return value (or null if no return)
     * @throws IOException on error
     * @throws IllegalAccessException on error
     * @throws InvocationTargetException on error
     */
    protected JsonNode invoke(Object target, Method m, List<JsonNode> params)
            throws IOException, IllegalAccessException, InvocationTargetException {

        // debug log
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "Invoking method: " + m.getName());
        }

        // convert the parameters
        Object[] convertedParams = new Object[params.size()];
        Type[] parameterTypes = m.getGenericParameterTypes();

        for (int i = 0; i < parameterTypes.length; i++) {
            JsonParser paramJsonParser = mapper.treeAsTokens(params.get(i));
            JavaType paramJavaType = TypeFactory.defaultInstance().constructType(parameterTypes[i]);
            convertedParams[i] = mapper.readValue(paramJsonParser, paramJavaType);
        }

        // invoke the method
        Object result = m.invoke(target, convertedParams);
        return (m.getGenericReturnType() != null) ? mapper.valueToTree(result) : null;
    }

    /**
     * Convenience method for creating an error response.
     *
     * @param jsonRpc the jsonrpc string
     * @param id the id
     * @param code the error code
     * @param message the error message
     * @param data the error data (if any)
     * @return the error response
     */
    protected ObjectNode createErrorResponse(String jsonRpc, Object id, int code, String message, Object data) {
        ObjectNode response = mapper.createObjectNode();
        ObjectNode error = mapper.createObjectNode();
        error.put("code", code);
        error.put("message", message);
        if (data != null) {
            error.put("data", mapper.valueToTree(data));
        }
        response.put("jsonrpc", jsonRpc);
        if (Integer.class.isInstance(id)) {
            response.put("id", Integer.class.cast(id).intValue());
        } else if (Long.class.isInstance(id)) {
            response.put("id", Long.class.cast(id).longValue());
        } else if (Float.class.isInstance(id)) {
            response.put("id", Float.class.cast(id).floatValue());
        } else if (Double.class.isInstance(id)) {
            response.put("id", Double.class.cast(id).doubleValue());
        } else if (BigDecimal.class.isInstance(id)) {
            response.put("id", BigDecimal.class.cast(id));
        } else {
            response.put("id", String.class.cast(id));
        }
        response.put("error", error);
        return response;
    }

    /**
     * Creates a sucess response.
     * @param jsonRpc
     * @param id
     * @param result
     * @return
     */
    protected ObjectNode createSuccessResponse(String jsonRpc, Object id, JsonNode result) {
        ObjectNode response = mapper.createObjectNode();
        response.put("jsonrpc", jsonRpc);
        if (Integer.class.isInstance(id)) {
            response.put("id", Integer.class.cast(id).intValue());
        } else if (Long.class.isInstance(id)) {
            response.put("id", Long.class.cast(id).longValue());
        } else if (Float.class.isInstance(id)) {
            response.put("id", Float.class.cast(id).floatValue());
        } else if (Double.class.isInstance(id)) {
            response.put("id", Double.class.cast(id).doubleValue());
        } else if (BigDecimal.class.isInstance(id)) {
            response.put("id", BigDecimal.class.cast(id));
        } else {
            response.put("id", String.class.cast(id));
        }
        response.put("result", result);
        return response;
    }

    /**
     * Finds the {@link Method} from the supplied {@link Set} that
     * best matches the rest of the arguments supplied and returns
     * it as a {@link MethodAndArgs} class.
     *
     * @param methods the {@link Method}s
     * @param paramsNode the {@link JsonNode} passed as the parameters
     * @return the {@link MethodAndArgs}
     */
    private MethodAndArgs findBestMethodByParamsNode(Set<Method> methods, JsonNode paramsNode) {

        // no parameters
        if (paramsNode == null || paramsNode.isNull()) {
            return findBestMethodUsingParamIndexes(methods, 0, null);

            // array parameters
        } else if (paramsNode.isArray()) {
            return findBestMethodUsingParamIndexes(methods, paramsNode.size(), ArrayNode.class.cast(paramsNode));

            // named parameters
        } else if (paramsNode.isObject()) {
            Set<String> fieldNames = new HashSet<String>();
            Iterator<String> itr = paramsNode.fieldNames();
            while (itr.hasNext()) {
                fieldNames.add(itr.next());
            }
            return findBestMethodUsingParamNames(methods, fieldNames, ObjectNode.class.cast(paramsNode));

        }

        // unknown params node type
        throw new IllegalArgumentException("Unknown params node type: " + paramsNode.toString());
    }

    /**
     * Finds the {@link Method} from the supplied {@link Set} that
     * best matches the rest of the arguments supplied and returns
     * it as a {@link MethodAndArgs} class.
     *
     * @param methods the {@link Method}s
     * @param paramCount the number of expect parameters
     * @param paramNodes the parameters for matching types
     * @return the {@link MethodAndArgs}
     */
    private MethodAndArgs findBestMethodUsingParamIndexes(Set<Method> methods, int paramCount,
            ArrayNode paramNodes) {

        // get param count
        int numParams = paramNodes != null && !paramNodes.isNull() ? paramNodes.size() : 0;

        // determine param count
        int bestParamNumDiff = Integer.MAX_VALUE;
        Set<Method> matchedMethods = new HashSet<Method>();

        // check every method
        for (Method method : methods) {

            // get parameter types
            Class<?>[] paramTypes = method.getParameterTypes();
            int paramNumDiff = paramTypes.length - paramCount;

            // we've already found a better match
            if (Math.abs(paramNumDiff) > Math.abs(bestParamNumDiff)) {
                continue;

                // we don't allow extra params
            } else if (!allowExtraParams && paramNumDiff < 0 || !allowLessParams && paramNumDiff > 0) {
                continue;

                // check the parameters
            } else {
                if (Math.abs(paramNumDiff) < Math.abs(bestParamNumDiff)) {
                    matchedMethods.clear();
                }
                matchedMethods.add(method);
                bestParamNumDiff = paramNumDiff;
                continue;
            }
        }

        // bail early
        if (matchedMethods.isEmpty()) {
            return null;
        }

        // now narrow it down to the best method
        // based on argument types
        Method bestMethod = null;
        if (matchedMethods.size() == 1 || numParams == 0) {
            bestMethod = matchedMethods.iterator().next();

        } else {

            // check the matching methods for
            // matching parameter types
            int mostMatches = -1;
            for (Method method : matchedMethods) {
                List<Class<?>> parameterTypes = getParameterTypes(method);
                int numMatches = 0;
                for (int i = 0; i < parameterTypes.size() && i < numParams; i++) {
                    if (isMatchingType(paramNodes.get(i), parameterTypes.get(i))) {
                        numMatches++;
                    }
                }
                if (numMatches > mostMatches) {
                    mostMatches = numMatches;
                    bestMethod = method;
                }
            }
        }

        // create return
        MethodAndArgs ret = new MethodAndArgs();
        ret.method = bestMethod;

        // now fill arguments
        int numParameters = bestMethod.getParameterTypes().length;
        for (int i = 0; i < numParameters; i++) {
            if (i < numParams) {
                ret.arguments.add(paramNodes.get(i));
            } else {
                ret.arguments.add(NullNode.getInstance());
            }
        }

        // return the method
        return ret;
    }

    /**
     * Finds the {@link Method} from the supplied {@link Set} that
     * best matches the rest of the arguments supplied and returns
     * it as a {@link MethodAndArgs} class.
     *
     * @param methods the {@link Method}s
     * @param paramNames the parameter names
     * @param paramNodes the parameters for matching types
     * @return the {@link MethodAndArgs}
     */
    @SuppressWarnings("deprecation")
    private MethodAndArgs findBestMethodUsingParamNames(Set<Method> methods, Set<String> paramNames,
            ObjectNode paramNodes) {

        // determine param count
        int maxMatchingParams = -1;
        int maxMatchingParamTypes = -1;
        Method bestMethod = null;
        List<JsonRpcParam> bestAnnotations = null;

        for (Method method : methods) {

            // get parameter types
            List<Class<?>> parameterTypes = getParameterTypes(method);

            // bail early if possible
            if (!allowExtraParams && paramNames.size() > parameterTypes.size()) {
                continue;
            } else if (!allowLessParams && paramNames.size() < parameterTypes.size()) {
                continue;
            }

            // list of params
            List<JsonRpcParam> annotations = new ArrayList<JsonRpcParam>();

            // try the deprecated parameter first
            List<List<JsonRpcParamName>> depMethodAnnotations = getParameterAnnotations(method,
                    JsonRpcParamName.class);
            for (List<JsonRpcParamName> annots : depMethodAnnotations) {
                if (annots.size() > 0) {
                    final JsonRpcParamName annotation = annots.get(0);
                    annotations.add(new JsonRpcParam() {
                        public Class<? extends Annotation> annotationType() {
                            return JsonRpcParam.class;
                        }

                        public String value() {
                            return annotation.value();
                        }
                    });
                } else {
                    annots.add(null);
                }
            }

            @SuppressWarnings("unchecked")
            List<List<Annotation>> jaxwsAnnotations = WEBPARAM_ANNOTATION_CLASS != null
                    ? getParameterAnnotations(method, (Class<Annotation>) WEBPARAM_ANNOTATION_CLASS)
                    : new ArrayList<List<Annotation>>();
            for (List<Annotation> annots : jaxwsAnnotations) {
                if (annots.size() > 0) {
                    final Annotation annotation = annots.get(0);
                    annotations.add(new JsonRpcParam() {
                        public Class<? extends Annotation> annotationType() {
                            return JsonRpcParam.class;
                        }

                        public String value() {
                            try {
                                return (String) WEBPARAM_NAME_METHOD.invoke(annotation);
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    });
                } else {
                    annots.add(null);
                }
            }

            // now try the non-deprecated parameters
            List<List<JsonRpcParam>> methodAnnotations = getParameterAnnotations(method, JsonRpcParam.class);
            for (List<JsonRpcParam> annots : methodAnnotations) {
                if (annots.size() > 0) {
                    annotations.add(annots.get(0));
                } else {
                    annots.add(null);
                }
            }

            // count the matching params for this method
            int numMatchingParamTypes = 0;
            int numMatchingParams = 0;
            for (int i = 0; i < annotations.size(); i++) {

                // skip parameters that didn't have an annotation
                JsonRpcParam annotation = annotations.get(i);
                if (annotation == null) {
                    continue;
                }

                // check for a match
                String paramName = annotation.value();
                boolean hasParamName = paramNames.contains(paramName);

                if (hasParamName && isMatchingType(paramNodes.get(paramName), parameterTypes.get(i))) {
                    numMatchingParamTypes++;
                    numMatchingParams++;

                } else if (hasParamName) {
                    numMatchingParams++;

                }
            }

            // check for exact param matches
            // bail early if possible
            if (!allowExtraParams && numMatchingParams > parameterTypes.size()) {
                continue;
            } else if (!allowLessParams && numMatchingParams < parameterTypes.size()) {
                continue;
            }

            // better match
            if (numMatchingParams > maxMatchingParams
                    || (numMatchingParams == maxMatchingParams && numMatchingParamTypes > maxMatchingParamTypes)) {
                bestMethod = method;
                maxMatchingParams = numMatchingParams;
                maxMatchingParamTypes = numMatchingParamTypes;
                bestAnnotations = annotations;
            }
        }

        // bail early
        if (bestMethod == null) {
            return null;
        }

        // create return
        MethodAndArgs ret = new MethodAndArgs();
        ret.method = bestMethod;

        // now fill arguments
        int numParameters = bestMethod.getParameterTypes().length;
        for (int i = 0; i < numParameters; i++) {
            JsonRpcParam param = bestAnnotations.get(i);
            if (param != null && paramNames.contains(param.value())) {
                ret.arguments.add(paramNodes.get(param.value()));
            } else {
                ret.arguments.add(NullNode.getInstance());
            }
        }

        // return the method
        return ret;
    }

    /**
     * Determines whether or not the given {@link JsonNode} matches
     * the given type.  This method is limitted to a few java types
     * only and shouldn't be used to determine with great accuracy
     * whether or not the types match.
     *
     * @param node the {@link JsonNode}
     * @param type the {@link Class}
     * @return true if the types match, false otherwise
     */
    private boolean isMatchingType(JsonNode node, Class<?> type) {

        if (node.isNull()) {
            return true;

        } else if (node.isTextual()) {
            return String.class.isAssignableFrom(type);

        } else if (node.isNumber()) {
            return Number.class.isAssignableFrom(type) || short.class.isAssignableFrom(type)
                    || int.class.isAssignableFrom(type) || long.class.isAssignableFrom(type)
                    || float.class.isAssignableFrom(type) || double.class.isAssignableFrom(type);

        } else if (node.isArray() && type.isArray()) {
            return (node.size() > 0) ? isMatchingType(node.get(0), type.getComponentType()) : false;

        } else if (node.isArray()) {
            return type.isArray() || Collection.class.isAssignableFrom(type);

        } else if (node.isBinary()) {
            return byte[].class.isAssignableFrom(type) || Byte[].class.isAssignableFrom(type)
                    || char[].class.isAssignableFrom(type) || Character[].class.isAssignableFrom(type);

        } else if (node.isBoolean()) {
            return boolean.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type);

        } else if (node.isObject() || node.isPojo()) {
            return !type.isPrimitive() && !String.class.isAssignableFrom(type)
                    && !Number.class.isAssignableFrom(type) && !Boolean.class.isAssignableFrom(type);
        }

        // not sure if it's a matching type
        return false;
    }

    /**
     * Writes and flushes a value to the given {@link OutputStream}
     * and prevents Jackson from closing it.
     * @param ops the {@link OutputStream}
     * @param value the value to write
     * @throws IOException on error
     */
    private void writeAndFlushValue(OutputStream ops, Object value) throws IOException {
        mapper.writeValue(new NoCloseOutputStream(ops), value);
        ops.flush();
    }

    /**
     * Simple inner class for the {@code findXXX} methods.
     */
    private static class MethodAndArgs {
        private Method method = null;
        private List<JsonNode> arguments = new ArrayList<JsonNode>();
    }

    /**
     * Parses an ID.
     * @param node
     * @return
     */
    private Object parseId(JsonNode node) {
        if (node == null || node.isNull()) {
            return null;
        } else if (node.isDouble()) {
            return node.asDouble();
        } else if (node.isFloatingPointNumber()) {
            return node.asDouble();
        } else if (node.isInt()) {
            return node.asInt();
        } else if (node.isIntegralNumber()) {
            return node.asInt();
        } else if (node.isLong()) {
            return node.asLong();
        } else if (node.isTextual()) {
            return node.asText();
        }
        throw new IllegalArgumentException("Unknown id type");
    }

    /**
     * Sets whether or not the server should be backwards
     * compatible to JSON-RPC 1.0.  This only includes the
     * omission of the jsonrpc property on the request object,
     * not the class hinting.
     *
     * @param backwardsComaptible the backwardsComaptible to set
     */
    public void setBackwardsComaptible(boolean backwardsComaptible) {
        this.backwardsComaptible = backwardsComaptible;
    }

    /**
     * Sets whether or not the server should re-throw exceptions.
     *
     * @param rethrowExceptions true or false
     */
    public void setRethrowExceptions(boolean rethrowExceptions) {
        this.rethrowExceptions = rethrowExceptions;
    }

    /**
     * Sets whether or not the server should allow superfluous
     * parameters to method calls.
     *
     * @param allowExtraParams true or false
     */
    public void setAllowExtraParams(boolean allowExtraParams) {
        this.allowExtraParams = allowExtraParams;
    }

    /**
     * Sets whether or not the server should allow less parameters
     * than required to method calls (passing null for missing params).
     *
     * @param allowLessParams the allowLessParams to set
     */
    public void setAllowLessParams(boolean allowLessParams) {
        this.allowLessParams = allowLessParams;
    }

    /**
     * Sets the {@link ErrorResolver} used for resolving errors.
     * Multiple {@link ErrorResolver}s can be used at once by
     * using the {@link MultipleErrorResolver}.
     *
     * @param errorResolver the errorResolver to set
     * @see MultipleErrorResolver
     */
    public void setErrorResolver(ErrorResolver errorResolver) {
        this.errorResolver = errorResolver;
    }

    /**
     * @param exceptionLogLevel the exceptionLogLevel to set
     */
    public void setExceptionLogLevel(Level exceptionLogLevel) {
        this.exceptionLogLevel = exceptionLogLevel;
    }

    /**
     * Sets the {@link InvocationListener} instance that can be
     * used to provide feedback for capturing method-invocation
     * statistics.
     * @param invocationListener is the listener to set
     */

    public void setInvocationListener(InvocationListener invocationListener) {
        this.invocationListener = invocationListener;
    }
}