package com.googlecode.jsonrpc4j; 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.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.ErrorResolver.JsonError; import net.iharder.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.lang.reflect.UndeclaredThrowableException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.ERROR_NOT_HANDLED; import static com.googlecode.jsonrpc4j.ReflectionUtil.findCandidateMethods; import static com.googlecode.jsonrpc4j.ReflectionUtil.getParameterTypes; import static com.googlecode.jsonrpc4j.Util.hasNonNullData; /** * A JSON-RPC request server reads JSON-RPC requests from an input stream and writes responses to an output stream. * Can even run on Android system. */ @SuppressWarnings({"unused", "WeakerAccess"}) public class JsonRpcBasicServer { public static final String JSONRPC_CONTENT_TYPE = "application/json-rpc"; public static final String PARAMS = "params"; public static final String METHOD = "method"; public static final String JSONRPC = "jsonrpc"; public static final String ID = "id"; public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String ACCEPT_ENCODING = "Accept-Encoding"; public static final String ERROR = "error"; public static final String ERROR_MESSAGE = "message"; public static final String ERROR_CODE = "code"; public static final String DATA = "data"; public static final String RESULT = "result"; public static final String EXCEPTION_TYPE_NAME = "exceptionTypeName"; public static final String VERSION = "2.0"; public static final int CODE_OK = 0; public static final String WEB_PARAM_ANNOTATION_CLASS_LOADER = "javax.jws.WebParam"; public static final String NAME = "name"; public static final String NULL = "null"; private static final Logger logger = LoggerFactory.getLogger(JsonRpcBasicServer.class); private static final ErrorResolver DEFAULT_ERROR_RESOLVER = new MultipleErrorResolver(AnnotationsErrorResolver.INSTANCE, DefaultErrorResolver.INSTANCE); private static Pattern BASE64_PATTERN = Pattern.compile("[A-Za-z0-9_=-]+"); private static Class<? extends Annotation> WEB_PARAM_ANNOTATION_CLASS; private static Method WEB_PARAM_NAME_METHOD; static { loadAnnotationSupportEngine(); } private final ObjectMapper mapper; private final Class<?> remoteInterface; private final Object handler; protected HttpStatusCodeProvider httpStatusCodeProvider = null; private boolean backwardsCompatible = true; private boolean rethrowExceptions = false; private boolean allowExtraParams = false; private boolean allowLessParams = false; private RequestInterceptor requestInterceptor = null; private ErrorResolver errorResolver = null; private InvocationListener invocationListener = null; private ConvertedParameterTransformer convertedParameterTransformer = null; private boolean shouldLogInvocationErrors = true; /** * 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 JsonRpcBasicServer(final ObjectMapper mapper, final Object handler) { this(mapper, handler, null); } /** * 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 JsonRpcBasicServer(final ObjectMapper mapper, final Object handler, final Class<?> remoteInterface) { this.mapper = mapper; this.handler = handler; this.remoteInterface = remoteInterface; if (handler != null) logger.debug("created server for interface {} with handler {}", remoteInterface, handler.getClass()); } /** * 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 JsonRpcBasicServer(final Object handler, final Class<?> remoteInterface) { this(new ObjectMapper(), handler, remoteInterface); } /** * Creates the server with a default {@link ObjectMapper} delegating * all calls to the given {@code handler}. * * @param handler the {@code handler} */ public JsonRpcBasicServer(final Object handler) { this(new ObjectMapper(), handler, null); } private static void loadAnnotationSupportEngine() { final ClassLoader classLoader = JsonRpcBasicServer.class.getClassLoader(); try { WEB_PARAM_ANNOTATION_CLASS = classLoader.loadClass(WEB_PARAM_ANNOTATION_CLASS_LOADER).asSubclass(Annotation.class); WEB_PARAM_NAME_METHOD = WEB_PARAM_ANNOTATION_CLASS.getMethod(NAME); } catch (ClassNotFoundException | NoSuchMethodException e) { logger.error("Could not find {}.{}", WEB_PARAM_ANNOTATION_CLASS_LOADER, NAME, e); } } /** * 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 */ static InputStream createInputStream(String method, String id, String params) throws IOException { StringBuilder envelope = new StringBuilder(); envelope.append("{\""); envelope.append(JSONRPC); envelope.append("\":\""); envelope.append(VERSION); envelope.append("\",\""); envelope.append(ID); envelope.append("\":"); // the 'id' value is assumed to be numerical. if (null != id && 0 != id.length()) { envelope.append(id); } else { envelope.append("null"); } envelope.append(",\""); envelope.append(METHOD); envelope.append("\":"); if (null != method && 0 != method.length()) { envelope.append('"'); envelope.append(method); envelope.append('"'); } else { envelope.append("null"); } envelope.append(",\""); envelope.append(PARAMS); envelope.append("\":"); if (null != params && 0 != params.length()) { String decodedParams; // some specifications suggest that the GET "params" query parameter should be Base64 encoded and // some suggest not. Try to deal with both scenarios -- the code here was previously only doing // Base64 decoding. // http://www.simple-is-better.org/json-rpc/transport_http.html // http://www.jsonrpc.org/historical/json-rpc-over-http.html#encoded-parameters if (BASE64_PATTERN.matcher(params).matches()) { decodedParams = new String(Base64.decode(params), StandardCharsets.UTF_8); } else { switch (params.charAt(0)) { case '[': case '{': decodedParams = params; break; default: throw new IOException("badly formed 'param' parameter starting with; [" + params.charAt(0) + "]"); } } envelope.append(decodedParams); } else { envelope.append("[]"); } envelope.append('}'); return new ByteArrayInputStream(envelope.toString().getBytes(StandardCharsets.UTF_8)); } public RequestInterceptor getRequestInterceptor() { return requestInterceptor; } public void setRequestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptor = requestInterceptor; } /** * 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 input the {@link InputStream} * @param output the {@link OutputStream} * @return the error code, or {@code 0} if none * @throws IOException on error */ public int handleRequest(final InputStream input, final OutputStream output) throws IOException { final ReadContext readContext = ReadContext.getReadContext(input, mapper); try { readContext.assertReadable(); final JsonNode jsonNode = readContext.nextValue(); return handleJsonNodeRequest(jsonNode, output).code; } catch (JsonParseException e) { return writeAndFlushValueError(output, createResponseError(JSONRPC, NULL, JsonError.PARSE_ERROR)).code; } } /** * 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(final 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 output the {@link OutputStream} * @return the error code, or {@code 0} if none * @throws IOException on error */ protected JsonError handleJsonNodeRequest(final JsonNode node, final OutputStream output) throws IOException { if (node.isArray()) return handleArray(ArrayNode.class.cast(node), output); if (node.isObject()) return handleObject(ObjectNode.class.cast(node), output); return this.writeAndFlushValueError(output, this.createResponseError(VERSION, NULL, JsonError.INVALID_REQUEST)); } /** * Handles the given {@link ArrayNode} and writes the * responses to the given {@link OutputStream}. * * @param node the {@link JsonNode} * @param output the {@link OutputStream} * @return the error code, or {@code 0} if none * @throws IOException on error */ private JsonError handleArray(ArrayNode node, OutputStream output) throws IOException { logger.debug("Handling {} requests", node.size()); JsonError result = JsonError.OK; output.write('['); int errorCount = 0; for (int i = 0; i < node.size(); i++) { JsonError nodeResult = handleJsonNodeRequest(node.get(i), output); if (isError(nodeResult)) { result = JsonError.BULK_ERROR; errorCount += 1; } if (i != node.size() - 1) output.write(','); } output.write(']'); logger.debug("served {} requests, error {}, result {}", node.size(), errorCount, result); // noinspection unchecked return result; } private boolean isError(JsonError result) { return result.code != JsonError.OK.code; } /** * Handles the given {@link ObjectNode} and writes the * responses to the given {@link OutputStream}. * * @param node the {@link JsonNode} * @param output the {@link OutputStream} * @return the error code, or {@code 0} if none * @throws IOException on error */ private JsonError handleObject(final ObjectNode node, final OutputStream output) throws IOException { logger.debug("Request: {}", node); if (!isValidRequest(node)) return writeAndFlushValueError(output, createResponseError(VERSION, NULL, JsonError.INVALID_REQUEST)); Object id = parseId(node.get(ID)); String jsonRpc = hasNonNullData(node, JSONRPC) ? node.get(JSONRPC).asText() : VERSION; if (!hasNonNullData(node, METHOD)) return writeAndFlushValueError(output, createResponseError(jsonRpc, id, JsonError.METHOD_NOT_FOUND)); final String fullMethodName = node.get(METHOD).asText(); final String partialMethodName = getMethodName(fullMethodName); final String serviceName = getServiceName(fullMethodName); Set<Method> methods = findCandidateMethods(getHandlerInterfaces(serviceName), partialMethodName); if (methods.isEmpty()) return writeAndFlushValueError(output, createResponseError(jsonRpc, id, JsonError.METHOD_NOT_FOUND)); AMethodWithItsArgs methodArgs = findBestMethodByParamsNode(methods, node.get(PARAMS)); if (methodArgs == null) return writeAndFlushValueError(output, createResponseError(jsonRpc, id, JsonError.METHOD_PARAMS_INVALID)); try (InvokeListenerHandler handler = new InvokeListenerHandler(methodArgs, invocationListener)) { try { if (this.requestInterceptor != null) this.requestInterceptor.interceptRequest(node); handler.result = invoke(getHandler(serviceName), methodArgs.method, methodArgs.arguments); if (!isNotificationRequest(id)) { ObjectNode response = createResponseSuccess(jsonRpc, id, handler.result); writeAndFlushValue(output, response); } return JsonError.OK; } catch (Throwable e) { handler.error = e; return handleError(output, id, jsonRpc, methodArgs, e); } } } private JsonError handleError(OutputStream output, Object id, String jsonRpc, AMethodWithItsArgs methodArgs, Throwable e) throws IOException { Throwable unwrappedException = getException(e); if (shouldLogInvocationErrors) { logger.warn("Error in JSON-RPC Service", unwrappedException); } JsonError error = resolveError(methodArgs, unwrappedException); writeAndFlushValueError(output, createResponseError(jsonRpc, id, error)); if (rethrowExceptions) { throw new RuntimeException(unwrappedException); } return error; } private Throwable getException(final Throwable thrown) { Throwable e = thrown; while (InvocationTargetException.class.isInstance(e)) { // noinspection ThrowableResultOfMethodCallIgnored e = InvocationTargetException.class.cast(e).getTargetException(); while (UndeclaredThrowableException.class.isInstance(e)) { // noinspection ThrowableResultOfMethodCallIgnored e = UndeclaredThrowableException.class.cast(e).getUndeclaredThrowable(); } } return e; } private JsonError resolveError(AMethodWithItsArgs methodArgs, Throwable e) { JsonError error; final ErrorResolver currentResolver = errorResolver == null ? DEFAULT_ERROR_RESOLVER : errorResolver; error = currentResolver.resolveError(e, methodArgs.method, methodArgs.arguments); if (error == null) { error = new JsonError(ERROR_NOT_HANDLED.code, e.getMessage(), e.getClass().getName()); } return error; } private boolean isNotificationRequest(Object id) { return id == null; } private boolean isValidRequest(ObjectNode node) { return backwardsCompatible || hasMethodAndVersion(node); } private boolean hasMethodAndVersion(ObjectNode node) { return node.has(JSONRPC) && node.has(METHOD); } /** * 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 methodName the JsonNode for the method * @return the name of the service, or <code>null</code> */ protected String getServiceName(final String methodName) { return null; } /** * Get the method name from the methodNode. * * @param methodName the JsonNode for the method * @return the name of the method that should be invoked */ protected String getMethodName(final String methodName) { return methodName; } /** * 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; } private static Class getJavaTypeForJsonType(JsonNodeType jsonType) { switch (jsonType) { case ARRAY: return List.class; case BINARY: return Object.class; case BOOLEAN: return Boolean.class; case MISSING: return Object.class; case NULL: return Object.class; case NUMBER: return Double.class; case OBJECT: return Object.class; case POJO: return Object.class; case STRING: return String.class; default: return Object.class; } } /** * Invokes the given method on the {@code handler} passing * the given params (after converting them to beans\objects) * to it. * * @param target optional service name used to locate the target object * to invoke the Method on * @param method 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 */ private JsonNode invoke(Object target, Method method, List<JsonNode> params) throws IOException, IllegalAccessException, InvocationTargetException { logger.debug("Invoking method: {} with args {}", method.getName(), params); Object[] convertedParams; Object result; if (method.getGenericParameterTypes().length == 1 && method.isVarArgs()) { convertedParams = new Object[params.size()]; // ObjectMapper mapper = new ObjectMapper(); for (int i = 0; i < params.size(); i++) { JsonNode jsonNode = params.get(i); Class<?> type = getJavaTypeForJsonType(jsonNode.getNodeType()); Object object = mapper.convertValue(jsonNode, type); logger.debug(String.format("[%s] param: %s -> %s", method.getName(), i, type.getName())); convertedParams[i] = object; } result = method.invoke(target, new Object[] {convertedParams}); } else { convertedParams = convertJsonToParameters(method, params); if (convertedParameterTransformer != null) { convertedParams = convertedParameterTransformer.transformConvertedParameters(target, convertedParams); } result = method.invoke(target, convertedParams); } logger.debug("Invoked method: {}, result {}", method.getName(), result); return hasReturnValue(method) ? mapper.valueToTree(result) : null; } private boolean hasReturnValue(Method m) { return m.getGenericReturnType() != null; } private Object[] convertJsonToParameters(Method m, List<JsonNode> params) throws IOException { 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 = mapper.getTypeFactory().constructType(parameterTypes[i]); convertedParams[i] = mapper.readValue(paramJsonParser, paramJavaType); } return convertedParams; } /** * Convenience method for creating an error response. * * @param jsonRpc the jsonrpc string * @param id the id * @param errorObject the error data (if any) * @return the error response */ private ErrorObjectWithJsonError createResponseError(String jsonRpc, Object id, JsonError errorObject) { ObjectNode response = mapper.createObjectNode(); ObjectNode error = mapper.createObjectNode(); error.put(ERROR_CODE, errorObject.code); error.put(ERROR_MESSAGE, errorObject.message); if (errorObject.data != null) { error.set(DATA, mapper.valueToTree(errorObject.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.set(ERROR, error); return new ErrorObjectWithJsonError(response, errorObject); } /** * Creates a success response. * * @param jsonRpc the version string * @param id the id of the request * @param result the result object * @return the response object */ private ObjectNode createResponseSuccess(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.set(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 AMethodWithItsArgs} class. * * @param methods the {@link Method}s * @param paramsNode the {@link JsonNode} passed as the parameters * @return the {@link AMethodWithItsArgs} */ private AMethodWithItsArgs findBestMethodByParamsNode(Set<Method> methods, JsonNode paramsNode) { if (hasNoParameters(paramsNode)) { return findBestMethodUsingParamIndexes(methods, 0, null); } AMethodWithItsArgs matchedMethod; if (paramsNode.isArray()) { matchedMethod = findBestMethodUsingParamIndexes(methods, paramsNode.size(), ArrayNode.class.cast(paramsNode)); } else if (paramsNode.isObject()) { matchedMethod = findBestMethodUsingParamNames(methods, collectFieldNames(paramsNode), ObjectNode.class.cast(paramsNode)); } else { throw new IllegalArgumentException("Unknown params node type: " + paramsNode.toString()); } if (matchedMethod == null) { matchedMethod = findBestMethodForVarargs(methods, paramsNode); } return matchedMethod; } private Set<String> collectFieldNames(JsonNode paramsNode) { Set<String> fieldNames = new HashSet<>(); Iterator<String> itr = paramsNode.fieldNames(); while (itr.hasNext()) { fieldNames.add(itr.next()); } return fieldNames; } private boolean hasNoParameters(JsonNode paramsNode) { return isNullNodeOrValue(paramsNode); } /** * Finds the {@link Method} from the supplied {@link Set} that * best matches the rest of the arguments supplied and returns * it as a {@link AMethodWithItsArgs} class. * * @param methods the {@link Method}s * @param paramCount the number of expect parameters * @param paramNodes the parameters for matching types * @return the {@link AMethodWithItsArgs} */ private AMethodWithItsArgs findBestMethodUsingParamIndexes(Set<Method> methods, int paramCount, ArrayNode paramNodes) { int numParams = isNullNodeOrValue(paramNodes) ? 0 : paramNodes.size(); int bestParamNumDiff = Integer.MAX_VALUE; Set<Method> matchedMethods = collectMethodsMatchingParamCount(methods, paramCount, bestParamNumDiff); if (matchedMethods.isEmpty()) return null; Method bestMethod = getBestMatchingArgTypeMethod(paramNodes, numParams, matchedMethods); return new AMethodWithItsArgs(bestMethod, paramCount, paramNodes); } private Method getBestMatchingArgTypeMethod(ArrayNode paramNodes, int numParams, Set<Method> matchedMethods) { if (matchedMethods.size() == 1 || numParams == 0) return matchedMethods.iterator().next(); Method bestMethod = null; int mostMatches = Integer.MIN_VALUE; for (Method method : matchedMethods) { List<Class<?>> parameterTypes = getParameterTypes(method); int numMatches = getNumArgTypeMatches(paramNodes, numParams, parameterTypes); if (hasMoreMatches(mostMatches, numMatches)) { mostMatches = numMatches; bestMethod = method; } } return bestMethod; } /** * Finds the {@link Method} from the supplied {@link Set} that * matches the method name annotation and have varargs. * it as a {@link AMethodWithItsArgs} class. * * @param methods the {@link Method}s * @param paramsNode the {@link JsonNode} of request * @return the {@link AMethodWithItsArgs} */ private AMethodWithItsArgs findBestMethodForVarargs(Set<Method> methods, JsonNode paramsNode) { for (Method method : methods) { if (method.getParameterTypes().length != 1) { continue; } if (method.isVarArgs()) { AMethodWithItsArgs matchedMethod = new AMethodWithItsArgs(method); if (paramsNode.isArray()) { ArrayNode arrayNode = ArrayNode.class.cast(paramsNode); for (int i = 0; i < paramsNode.size(); i++) { matchedMethod.addArgument(arrayNode.get(i)); } } if (paramsNode.isObject()) { ObjectNode objectNode = ObjectNode.class.cast(paramsNode); Iterator<Map.Entry<String,JsonNode>> items = objectNode.fields(); while (items.hasNext()) { Map.Entry<String,JsonNode> item = items.next(); JsonNode name = JsonNodeFactory.instance.objectNode().put(item.getKey(),item.getKey()); matchedMethod.addArgument(name.get(item.getKey())); matchedMethod.addArgument(item.getValue()); } } return matchedMethod; } } return null; } private int getNumArgTypeMatches(ArrayNode paramNodes, int numParams, List<Class<?>> parameterTypes) { int numMatches = 0; for (int i = 0; i < parameterTypes.size() && i < numParams; i++) { if (isMatchingType(paramNodes.get(i), parameterTypes.get(i))) { numMatches++; } } return numMatches; } private Set<Method> collectMethodsMatchingParamCount(Set<Method> methods, int paramCount, int bestParamNumDiff) { Set<Method> matchedMethods = new HashSet<>(); // check every method for (Method method : methods) { Class<?>[] paramTypes = method.getParameterTypes(); final int paramNumDiff = paramTypes.length - paramCount; if (hasLessOrEqualAbsParamDiff(bestParamNumDiff, paramNumDiff) && acceptParamCount(paramNumDiff)) { if (hasLessAbsParamDiff(bestParamNumDiff, paramNumDiff)) matchedMethods.clear(); matchedMethods.add(method); bestParamNumDiff = paramNumDiff; } } return matchedMethods; } private boolean hasLessAbsParamDiff(int bestParamNumDiff, int paramNumDiff) { return Math.abs(paramNumDiff) < Math.abs(bestParamNumDiff); } private boolean acceptParamCount(int paramNumDiff) { return paramNumDiff == 0 || acceptNonExactParam(paramNumDiff); } private boolean acceptNonExactParam(int paramNumDiff) { return acceptMoreParam(paramNumDiff) || acceptLessParam(paramNumDiff); } private boolean acceptLessParam(int paramNumDiff) { return allowLessParams && paramNumDiff > 0; } private boolean acceptMoreParam(int paramNumDiff) { return allowExtraParams && paramNumDiff < 0; } private boolean hasLessOrEqualAbsParamDiff(int bestParamNumDiff, int paramNumDiff) { return Math.abs(paramNumDiff) <= Math.abs(bestParamNumDiff); } /** * Finds the {@link Method} from the supplied {@link Set} that best matches the rest of the arguments supplied and * returns it as a {@link AMethodWithItsArgs} class. * * @param methods the {@link Method}s * @param paramNames the parameter allNames * @param paramNodes the parameters for matching types * @return the {@link AMethodWithItsArgs} */ private AMethodWithItsArgs findBestMethodUsingParamNames(Set<Method> methods, Set<String> paramNames, ObjectNode paramNodes) { ParameterCount max = new ParameterCount(); for (Method method : methods) { List<Class<?>> parameterTypes = getParameterTypes(method); int typeNameCountDiff = parameterTypes.size() - paramNames.size(); if (!acceptParamCount(typeNameCountDiff)) continue; ParameterCount parStat = new ParameterCount(paramNames, paramNodes, parameterTypes, method); if (!acceptParamCount(parStat.nameCount - paramNames.size())) continue; if (hasMoreMatches(max.nameCount, parStat.nameCount) || parStat.nameCount == max.nameCount && hasMoreMatches(max.typeCount, parStat.typeCount)) max = parStat; } if (max.method == null) return null; return new AMethodWithItsArgs(max.method, paramNames, max.allNames, paramNodes); } private boolean hasMoreMatches(int maxMatchingParams, int numMatchingParams) { return numMatchingParams > maxMatchingParams; } private boolean missingAnnotation(JsonRpcParam name) { return name == null; } /** * Determines whether or not the given {@link JsonNode} matches * the given type. This method is limited 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 */ @SuppressWarnings("SimplifiableIfStatement") private boolean isMatchingType(JsonNode node, Class<?> type) { if (node.isNull()) return true; if (node.isTextual()) return String.class.isAssignableFrom(type); if (node.isNumber()) return isNumericAssignable(type); if (node.isArray() && type.isArray()) return node.size() > 0 && isMatchingType(node.get(0), type.getComponentType()); if (node.isArray()) return type.isArray() || Collection.class.isAssignableFrom(type); if (node.isBinary()) return byteOrCharAssignable(type); if (node.isBoolean()) return boolean.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type); if (node.isObject() || node.isPojo()) { return !type.isPrimitive() && !String.class.isAssignableFrom(type) && !Number.class.isAssignableFrom(type) && !Boolean.class.isAssignableFrom(type); } return false; } private boolean byteOrCharAssignable(Class<?> type) { return byte[].class.isAssignableFrom(type) || Byte[].class.isAssignableFrom(type) || char[].class.isAssignableFrom(type) || Character[].class.isAssignableFrom(type); } private boolean isNumericAssignable(Class<?> type) { 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); } private JsonError writeAndFlushValueError(OutputStream output, ErrorObjectWithJsonError value) throws IOException { logger.debug("failed {}", value); writeAndFlushValue(output, value.node); return value.error; } /** * Writes and flushes a value to the given {@link OutputStream} * and prevents Jackson from closing it. Also writes newline. * * @param output the {@link OutputStream} * @param value the value to write * @throws IOException on error */ private void writeAndFlushValue(OutputStream output, Object value) throws IOException { logger.debug("Response: {}", value); mapper.writeValue(new NoCloseOutputStream(output), value); output.write('\n'); } private Object parseId(JsonNode node) { if (isNullNodeOrValue(node)) return null; if (node.isDouble()) return node.asDouble(); if (node.isFloatingPointNumber()) return node.asDouble(); if (node.isInt()) return node.asInt(); if (node.isIntegralNumber()) return node.asInt(); if (node.isLong()) return node.asLong(); if (node.isTextual()) return node.asText(); throw new IllegalArgumentException("Unknown id type"); } private boolean isNullNodeOrValue(JsonNode node) { return node == null || node.isNull(); } /** * 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 backwardsCompatible the backwardsCompatible to set */ public void setBackwardsCompatible(boolean backwardsCompatible) { this.backwardsCompatible = backwardsCompatible; } /** * 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; } /** * 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; } /** * Sets the {@link HttpStatusCodeProvider} instance to use for HTTP error results. * * @param httpStatusCodeProvider the status code provider to use for translating JSON-RPC error codes into * HTTP status messages. */ public void setHttpStatusCodeProvider(HttpStatusCodeProvider httpStatusCodeProvider) { this.httpStatusCodeProvider = httpStatusCodeProvider; } /** * Sets the {@link ConvertedParameterTransformer} instance that can be * used to mutate the deserialized arguments passed to the service method during invocation. * * @param convertedParameterTransformer the transformer to set */ public void setConvertedParameterTransformer(ConvertedParameterTransformer convertedParameterTransformer) { this.convertedParameterTransformer = convertedParameterTransformer; } /** * If true, then when errors arise in the invocation of JSON-RPC services, the error will be * logged together with the underlying stack trace. When false, no error will be logged. * An alternative mechanism for logging invocation errors is to employ an implementation of * {@link InvocationListener}. * * @param shouldLogInvocationErrors see method description */ public void setShouldLogInvocationErrors(boolean shouldLogInvocationErrors) { this.shouldLogInvocationErrors = shouldLogInvocationErrors; } private static class ErrorObjectWithJsonError { private final ObjectNode node; private final JsonError error; public ErrorObjectWithJsonError(ObjectNode node, JsonError error) { this.node = node; this.error = error; } @Override public String toString() { return "ErrorObjectWithJsonError{" + "node=" + node + ", error=" + error + '}'; } } /** * Simple inner class for the {@code findXXX} methods. */ private static class AMethodWithItsArgs { private final List<JsonNode> arguments = new ArrayList<>(); private final Method method; public AMethodWithItsArgs(Method method, int paramCount, ArrayNode paramNodes) { this(method); collectArgumentsBasedOnCount(method, paramCount, paramNodes); } public AMethodWithItsArgs(Method method) { this.method = method; } private void collectArgumentsBasedOnCount(Method method, int paramCount, ArrayNode paramNodes) { int numParameters = method.getParameterTypes().length; for (int i = 0; i < numParameters; i++) { if (i < paramCount) { addArgument(paramNodes.get(i)); } else { addArgument(NullNode.getInstance()); } } } public AMethodWithItsArgs(Method method, Set<String> paramNames, List<JsonRpcParam> allNames, ObjectNode paramNodes) { this(method); collectArgumentsBasedOnName(method, paramNames, allNames, paramNodes); } private void collectArgumentsBasedOnName(Method method, Set<String> paramNames, List<JsonRpcParam> allNames, ObjectNode paramNodes) { int numParameters = method.getParameterTypes().length; for (int i = 0; i < numParameters; i++) { JsonRpcParam param = allNames.get(i); if (param != null && paramNames.contains(param.value())) { addArgument(paramNodes.get(param.value())); } else { addArgument(NullNode.getInstance()); } } } public void addArgument(JsonNode argumentJsonNode) { arguments.add(argumentJsonNode); } } private static class InvokeListenerHandler implements AutoCloseable { private final long startMs = System.currentTimeMillis(); private final AMethodWithItsArgs methodArgs; private final InvocationListener invocationListener; public Throwable error = null; public JsonNode result = null; public InvokeListenerHandler(AMethodWithItsArgs methodArgs, InvocationListener invocationListener) { this.methodArgs = methodArgs; this.invocationListener = invocationListener; if (this.invocationListener != null) { this.invocationListener.willInvoke(methodArgs.method, methodArgs.arguments); } } @Override public void close() { if (invocationListener != null) { invocationListener.didInvoke(methodArgs.method, methodArgs.arguments, result, error, System.currentTimeMillis() - startMs); } } } private class ParameterCount { private final int typeCount; private final int nameCount; private final List<JsonRpcParam> allNames; private final Method method; public ParameterCount(Set<String> paramNames, ObjectNode paramNodes, List<Class<?>> parameterTypes, Method method) { this.allNames = getAnnotatedParameterNames(method); this.method = method; int typeCount = 0; int nameCount = 0; int at = 0; for (JsonRpcParam name : this.allNames) { if (missingAnnotation(name)) continue; String paramName = name.value(); boolean hasParamName = paramNames.contains(paramName); if (hasParamName) nameCount += 1; if (hasParamName && isMatchingType(paramNodes.get(paramName), parameterTypes.get(at))) typeCount += 1; at += 1; } this.typeCount = typeCount; this.nameCount = nameCount; } @SuppressWarnings("Convert2streamapi") private List<JsonRpcParam> getAnnotatedParameterNames(Method method) { List<JsonRpcParam> parameterNames = new ArrayList<>(); for (List<? extends Annotation> webParamAnnotation : getWebParameterAnnotations(method)) { if (!webParamAnnotation.isEmpty()) parameterNames.add(createNewJsonRcpParamType(webParamAnnotation.get(0))); } for (List<JsonRpcParam> annotation : getJsonRpcParamAnnotations(method)) { if (!annotation.isEmpty()) parameterNames.add(annotation.get(0)); } return parameterNames; } private List<? extends List<? extends Annotation>> getWebParameterAnnotations(Method method) { if (WEB_PARAM_ANNOTATION_CLASS == null) return new ArrayList<>(); return ReflectionUtil.getParameterAnnotations(method, WEB_PARAM_ANNOTATION_CLASS); } private JsonRpcParam createNewJsonRcpParamType(final Annotation annotation) { return new JsonRpcParam() { public Class<? extends Annotation> annotationType() { return JsonRpcParam.class; } public String value() { try { return (String) WEB_PARAM_NAME_METHOD.invoke(annotation); } catch (Exception e) { throw new RuntimeException(e); } } }; } private List<List<JsonRpcParam>> getJsonRpcParamAnnotations(Method method) { return ReflectionUtil.getParameterAnnotations(method, JsonRpcParam.class); } public ParameterCount() { typeCount = -1; nameCount = -1; allNames = null; method = null; } public int getTypeCount() { return typeCount; } public int getNameCount() { return nameCount; } } }