package com.googlecode.jsonrpc4j; 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.ObjectNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Type; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Random; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.ERROR; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.ID; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.JSONRPC; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.METHOD; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.PARAMS; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.RESULT; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.VERSION; import static com.googlecode.jsonrpc4j.Util.hasNonNullData; /** * A JSON-RPC client. */ @SuppressWarnings({"WeakerAccess", "unused"}) public class JsonRpcClient { // Toha: to use same logger in extension classes protected final Logger logger = LoggerFactory.getLogger(this.getClass()); private final ObjectMapper mapper; private final Random random; private RequestListener requestListener; private RequestIDGenerator requestIDGenerator; private ExceptionResolver exceptionResolver; private Map<String, Object> additionalJsonContent = new HashMap<>(); /** * Creates a client that uses the default {@link ObjectMapper} * to map to and from JSON and Java objects. */ public JsonRpcClient() { this(new ObjectMapper()); } /** * Creates a client that uses the given {@link ObjectMapper} to * map to and from JSON and Java objects. * * @param mapper the {@link ObjectMapper} */ public JsonRpcClient(ObjectMapper mapper) { this(mapper, DefaultExceptionResolver.INSTANCE); } /** * Creates a client that uses the given {@link ObjectMapper} and * {@link ExceptionResolver } to map to and from JSON and Java objects. * * @param mapper the {@link ObjectMapper} * @param exceptionResolver the {@link ExceptionResolver} */ public JsonRpcClient(ObjectMapper mapper, ExceptionResolver exceptionResolver) { this.mapper = mapper; this.random = new Random(System.currentTimeMillis()); this.requestIDGenerator = new RandomRequestIDGenerator(); this.exceptionResolver = exceptionResolver; } public Map<String, Object> getAdditionalJsonContent() { return additionalJsonContent; } public void setAdditionalJsonContent(Map<String, Object> additionalJsonContent) { this.additionalJsonContent = additionalJsonContent; } /** * Sets the {@link RequestListener}. * * @param requestListener the {@link RequestListener} */ public void setRequestListener(RequestListener requestListener) { this.requestListener = requestListener; } /** * Set the {@link RequestIDGenerator} * * @param requestIDGenerator the {@link RequestIDGenerator} */ public void setRequestIDGenerator(RequestIDGenerator requestIDGenerator) { this.requestIDGenerator = requestIDGenerator; } /** * Invokes the given method on the remote service * passing the given arguments, a generated id and reads * a response. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param clazz the expected return type * @param output the {@link OutputStream} to write to * @param input the {@link InputStream} to read from * @param <T> the expected return type * @return the returned Object * @throws Throwable on error * @see #writeRequest(String, Object, OutputStream, String) */ @SuppressWarnings("unchecked") public <T> T invokeAndReadResponse(String methodName, Object argument, Class<T> clazz, OutputStream output, InputStream input) throws Throwable { return (T) invokeAndReadResponse(methodName, argument, Type.class.cast(clazz), output, input); } /** * Invokes the given method on the remote service * passing the given arguments, a generated id and reads * a response. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param returnType the expected return type * @param output the {@link OutputStream} to write to * @param input the {@link InputStream} to read from * @return the returned Object * @throws Throwable on error * @see #writeRequest(String, Object, OutputStream, String) */ public Object invokeAndReadResponse(String methodName, Object argument, Type returnType, OutputStream output, InputStream input) throws Throwable { return invokeAndReadResponse(methodName, argument, returnType, output, input, this.requestIDGenerator.generateID()); } /** * Invokes the given method on the remote service * passing the given arguments and reads a response. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param returnType the expected return type * @param output the {@link OutputStream} to write to * @param input the {@link InputStream} to read from * @param id id to send with the JSON-RPC request * @return the returned Object * @throws Throwable if there is an error * while reading the response * @see #writeRequest(String, Object, OutputStream, String) */ private Object invokeAndReadResponse(String methodName, Object argument, Type returnType, OutputStream output, InputStream input, String id) throws Throwable { invoke(methodName, argument, output, id); return readResponse(returnType, input, id); } /** * Invokes the given method on the remote service passing * the given argument. To read the response * {@link #readResponse(Type, InputStream)} must be subsequently * called. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param output the {@link OutputStream} to write to * @param id the request id * @throws IOException on error * @see #writeRequest(String, Object, OutputStream, String) */ private void invoke(String methodName, Object argument, OutputStream output, String id) throws IOException { writeRequest(methodName, argument, output, id); output.flush(); } /** * Reads a JSON-PRC response from the server. This blocks until * a response is received. If an id is given, responses that do * not correspond, are disregarded. * * @param returnType the expected return type * @param input the {@link InputStream} to read from * @param id The id used to compare the response with. * @return the object returned by the JSON-RPC response * @throws Throwable on error */ private Object readResponse(Type returnType, InputStream input, String id) throws Throwable { ReadContext context = ReadContext.getReadContext(input, mapper); ObjectNode jsonObject = getValidResponse(id, context); notifyAnswerListener(jsonObject); handleErrorResponse(jsonObject); if (hasResult(jsonObject)) { if (isReturnTypeInvalid(returnType)) return null; return constructResponseObject(returnType, jsonObject); } // no return type return null; } /** * Writes a JSON-RPC request to the given {@link OutputStream}. * If the value passed for argument is null then the {@code params} * property is omitted from the JSON-RPC request. If the argument * is not not null then it is used as the value of the {@code params} * property. This means that if a POJO is passed as the argument * that it's properties will be used as the param, ie: * <pre> * class Person { * String firstName; * String lastName; * } * </pre> * becomes: * <pre> * "params" : { * "firstName" : ..; * "lastName" : ..; * } * </pre> * The same would be true of a {@link Map} containing the keys * {@code firstName} and {@code lastName}. If the argument passed * in implements the {@link Collection} interface or is an array * then the values are used as indexed parameters in the order that * they appear in the {@link Collection} or array. * * @param methodName the method to invoke * @param argument the method argument * @param output the {@link OutputStream} to write to * @param id the request id * @throws IOException on error */ private void writeRequest(String methodName, Object argument, OutputStream output, String id) throws IOException { internalWriteRequest(methodName, argument, output, id); } private ObjectNode getValidResponse(String id, ReadContext context) throws IOException { JsonNode response = readResponseNode(context); raiseExceptionIfNotValidResponseObject(response); ObjectNode jsonObject = ObjectNode.class.cast(response); if (id != null) { while (isIdValueNotCorrect(id, jsonObject)) { response = context.nextValue(); raiseExceptionIfNotValidResponseObject(response); jsonObject = ObjectNode.class.cast(response); } } return jsonObject; } private void notifyAnswerListener(ObjectNode jsonObject) { if (this.requestListener != null) { this.requestListener.onBeforeResponseProcessed(this, jsonObject); } } protected void handleErrorResponse(ObjectNode jsonObject) throws Throwable { if (hasError(jsonObject)) { // resolve and throw the exception if (exceptionResolver == null) { throw DefaultExceptionResolver.INSTANCE.resolveException(jsonObject); } else { throw exceptionResolver.resolveException(jsonObject); } } } private boolean hasResult(ObjectNode jsonObject) { return hasNonNullData(jsonObject, RESULT); } private boolean isReturnTypeInvalid(Type returnType) { if (returnType == null || returnType == Void.class) { logger.warn("Server returned result but returnType is null"); return true; } return false; } private Object constructResponseObject(Type returnType, ObjectNode jsonObject) throws IOException { JsonParser returnJsonParser = mapper.treeAsTokens(jsonObject.get(RESULT)); JavaType returnJavaType = mapper.getTypeFactory().constructType(returnType); return mapper.readValue(returnJsonParser, returnJavaType); } /** * Writes a request. * * @param methodName the method name * @param arguments the arguments * @param output the stream * @param id the optional id * @throws IOException on error */ private void internalWriteRequest(String methodName, Object arguments, OutputStream output, String id) throws IOException { final ObjectNode request = internalCreateRequest(methodName, arguments, id); logger.debug("Request {}", request); writeAndFlushValue(output, request); } private JsonNode readResponseNode(ReadContext context) throws IOException { context.assertReadable(); JsonNode response = context.nextValue(); logger.debug("JSON-PRC Response: {}", response.toString()); return response; } private void raiseExceptionIfNotValidResponseObject(JsonNode response) { if (isInvalidResponse(response)) { throw new JsonRpcClientException(0, "Invalid JSON-RPC response", response); } } private boolean isIdValueNotCorrect(String id, ObjectNode jsonObject) { return !jsonObject.has(ID) || jsonObject.get(ID) == null || !jsonObject.get(ID).asText().equals(id); } protected boolean hasError(ObjectNode jsonObject) { return jsonObject.has(ERROR) && jsonObject.get(ERROR) != null && !jsonObject.get(ERROR).isNull(); } /** * Creates RPC request. * * @param methodName the method name * @param arguments the arguments * @param id the optional id * @return Jackson request object */ private ObjectNode internalCreateRequest(String methodName, Object arguments, String id) { final ObjectNode request = mapper.createObjectNode(); addId(id, request); addProtocolAndMethod(methodName, request); addParameters(arguments, request); addAdditionalHeaders(request); notifyBeforeRequestListener(request); return request; } /** * Writes and flushes a value to the given {@link OutputStream} * and prevents Jackson from closing it. * * @param output the {@link OutputStream} * @param value the value to write * @throws IOException on error */ private void writeAndFlushValue(OutputStream output, Object value) throws IOException { mapper.writeValue(new NoCloseOutputStream(output), value); output.flush(); } private boolean isInvalidResponse(JsonNode response) { return !response.isObject(); } private void addId(String id, ObjectNode request) { if (id != null) { request.put(ID, id); } } private void addProtocolAndMethod(String methodName, ObjectNode request) { request.put(JSONRPC, VERSION); request.put(METHOD, methodName); } private void addParameters(Object arguments, ObjectNode request) { // object array args if (isArrayArguments(arguments)) { addArrayArguments(arguments, request); // collection args } else if (isCollectionArguments(arguments)) { addCollectionArguments(arguments, request); // map args } else if (isMapArguments(arguments)) { addMapArguments(arguments, request); // other args } else if (arguments != null) { request.set(PARAMS, mapper.valueToTree(arguments)); } } private void addAdditionalHeaders(ObjectNode request) { for (Map.Entry<String, Object> entry : additionalJsonContent.entrySet()) { request.set(entry.getKey(), mapper.valueToTree(entry.getValue())); } } private void notifyBeforeRequestListener(ObjectNode request) { if (this.requestListener != null) { this.requestListener.onBeforeRequestSent(this, request); } } private boolean isArrayArguments(Object arguments) { return arguments != null && arguments.getClass().isArray(); } private void addArrayArguments(Object arguments, ObjectNode request) { Object[] args = Object[].class.cast(arguments); if (args.length > 0) { // serialize every param for itself so jackson can determine right serializer ArrayNode paramsNode = new ArrayNode(mapper.getNodeFactory()); for (Object arg : args) { JsonNode argNode = mapper.valueToTree(arg); paramsNode.add(argNode); } request.set(PARAMS, paramsNode); } } private boolean isCollectionArguments(Object arguments) { return arguments != null && Collection.class.isInstance(arguments); } private void addCollectionArguments(Object arguments, ObjectNode request) { Collection<?> args = Collection.class.cast(arguments); if (!args.isEmpty()) { // serialize every param for itself so jackson can determine right serializer ArrayNode paramsNode = new ArrayNode(mapper.getNodeFactory()); for (Object arg : args) { JsonNode argNode = mapper.valueToTree(arg); paramsNode.add(argNode); } request.set(PARAMS, paramsNode); } } private boolean isMapArguments(Object arguments) { return arguments != null && Map.class.isInstance(arguments); } private void addMapArguments(Object arguments, ObjectNode request) { if (!Map.class.cast(arguments).isEmpty()) { request.set(PARAMS, mapper.valueToTree(arguments)); } } private String generateRandomId() { return random.nextInt(Integer.MAX_VALUE) + ""; } /** * Invokes the given method on the remote service * passing the given arguments and reads a response. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param clazz the expected return type * @param output the {@link OutputStream} to write to * @param input the {@link InputStream} to read from * @param id id to send with the JSON-RPC request * @param <T> the expected return type * @return the returned Object * @throws Throwable if there is an error * while reading the response * @see #writeRequest(String, Object, OutputStream, String) */ @SuppressWarnings("unchecked") public <T> T invokeAndReadResponse(String methodName, Object argument, Class<T> clazz, OutputStream output, InputStream input, String id) throws Throwable { return (T) invokeAndReadResponse(methodName, argument, Type.class.cast(clazz), output, input, id); } /** * Invokes the given method on the remote service passing * the given argument. An id is generated automatically. To read * the response {@link #readResponse(Type, InputStream)} must be * subsequently called. * * @param methodName the method to invoke * @param argument the arguments to pass to the method * @param output the {@link OutputStream} to write to * @throws IOException on error * @see #writeRequest(String, Object, OutputStream, String) */ public void invoke(String methodName, Object argument, OutputStream output) throws IOException { invoke(methodName, argument, output, this.requestIDGenerator.generateID()); } /** * Invokes the given method on the remote service passing * the given argument without reading or expecting a return * response. * * @param methodName the method to invoke * @param argument the argument to pass to the method * @param output the {@link OutputStream} to write to * @throws IOException on error * @see #writeRequest(String, Object, OutputStream, String) */ public void invokeNotification(String methodName, Object argument, OutputStream output) throws IOException { writeRequest(methodName, argument, output, null); output.flush(); } /** * Reads a JSON-PRC response from the server. This blocks until * a response is received. * * @param clazz the expected return type * @param input the {@link InputStream} to read from * @param <T> the expected return type * @return the object returned by the JSON-RPC response * @throws Throwable on error */ @SuppressWarnings("unchecked") public <T> T readResponse(Class<T> clazz, InputStream input) throws Throwable { return (T) readResponse((Type) clazz, input); } /** * Reads a JSON-PRC response from the server. This blocks until * a response is received. * * @param returnType the expected return type * @param input the {@link InputStream} to read from * @return the object returned by the JSON-RPC response * @throws Throwable on error */ public Object readResponse(Type returnType, InputStream input) throws Throwable { return readResponse(returnType, input, null); } /** * Reads a JSON-PRC response from the server. This blocks until * a response is received. If an id is given, responses that do * not correspond, are disregarded. * * @param clazz the expected return type * @param input the {@link InputStream} to read from * @param id The id used to compare the response with * @param <T> the expected return type * @return the object returned by the JSON-RPC response * @throws Throwable on error */ @SuppressWarnings("unchecked") public <T> T readResponse(Class<T> clazz, InputStream input, String id) throws Throwable { return (T) readResponse((Type) clazz, input, id); } protected ObjectNode createRequest(String methodName, Object argument) { return internalCreateRequest(methodName, argument, this.requestIDGenerator.generateID()); } public ObjectNode createRequest(String methodName, Object argument, String id) { return internalCreateRequest(methodName, argument, id); } /** * Writes a JSON-RPC notification to the given * {@link OutputStream}. * * @param methodName the method to invoke * @param argument the method argument * @param output the {@link OutputStream} to write to * @throws IOException on error * @see #writeRequest(String, Object, OutputStream, String) */ public void writeNotification(String methodName, Object argument, OutputStream output) throws IOException { internalWriteRequest(methodName, argument, output, null); } // Suppose than jsonObject is single and contains valid id :) protected Object readResponse(Type returnType, JsonNode jsonObject) throws Throwable { return readResponse(returnType, jsonObject, null); } // Suppose than jsonObject is single and contains valid id :) private Object readResponse(Type returnType, JsonNode jsonNode, String id) throws Throwable { raiseExceptionIfNotValidResponseObject(jsonNode); final ObjectNode jsonObject = ObjectNode.class.cast(jsonNode); notifyAnswerListener(jsonObject); handleErrorResponse(jsonObject); if (hasResult(jsonObject)) { if (isReturnTypeInvalid(returnType)) return null; return constructResponseObject(returnType, jsonObject); } return null; } /** * Returns the {@link ObjectMapper} that the client * is using for JSON marshalling. * * @return the {@link ObjectMapper} */ public ObjectMapper getObjectMapper() { return mapper; } /** * @param exceptionResolver the exceptionResolver to set */ public void setExceptionResolver(ExceptionResolver exceptionResolver) { this.exceptionResolver = exceptionResolver; } /** * Provides access to the jackson {@link ObjectNode}s * that represent the JSON-RPC requests and responses. */ public interface RequestListener { /** * Called before a request is sent to the * server end-point. Modifications can be * made to the request before it's sent. * * @param client the {@link JsonRpcClient} * @param request the request {@link ObjectNode} */ void onBeforeRequestSent(JsonRpcClient client, ObjectNode request); /** * Called after a response has been returned and * successfully parsed but before it has been * processed and turned into java objects. * * @param client the {@link JsonRpcClient} * @param response the response {@link ObjectNode} */ void onBeforeResponseProcessed(JsonRpcClient client, ObjectNode response); } /** * Default generator which returns random generated request ID. */ class RandomRequestIDGenerator implements RequestIDGenerator { @Override public String generateID() { return generateRandomId(); } } }