From e85f323cf214d4976fd87b215346e32513abda5c Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Sat, 23 Sep 2023 14:20:55 -0700 Subject: [PATCH] JsonRpcClient: significant refactoring, Jackson decoupling 1. Define JsonRpcTransport interface 2. JsonRpcClient is now generic 3. JacksonRpcClient is removed (sorry no deprecation) 4. AbstractRpcClient remains Jackson-aware --- .../groovy/BitcoinScriptingClient.groovy | 3 +- .../bitcoin/jsonrpc/BitcoinClient.java | 5 +- .../jsonrpc/BitcoinExtendedClient.java | 7 +- .../bitcoin/rx/jsonrpc/RxBitcoinClient.java | 3 +- .../jsonrpc/groovy/DynamicRpcClient.groovy | 4 +- .../groovy/DynamicRpcMethodFallback.groovy | 2 +- .../consensusj/jsonrpc/AbstractRpcClient.java | 62 +++--- .../consensusj/jsonrpc/JacksonRpcClient.java | 179 ------------------ .../org/consensusj/jsonrpc/JsonRpcClient.java | 139 +++++++++----- .../JsonRpcClientHttpUrlConnection.java | 7 +- .../jsonrpc/JsonRpcClientJavaNet.java | 4 +- .../JsonRpcClientHttpUrlConnectionSpec.groovy | 2 +- ...pec.groovy => JsonRpcTransportSpec.groovy} | 4 +- 13 files changed, 148 insertions(+), 273 deletions(-) delete mode 100644 consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JacksonRpcClient.java rename consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/{AbstractRpcClientSpec.groovy => JsonRpcTransportSpec.groovy} (83%) diff --git a/cj-btc-jsonrpc-gvy/src/main/groovy/org/consensusj/bitcoin/jsonrpc/groovy/BitcoinScriptingClient.groovy b/cj-btc-jsonrpc-gvy/src/main/groovy/org/consensusj/bitcoin/jsonrpc/groovy/BitcoinScriptingClient.groovy index a688f05a2..5c7da9676 100644 --- a/cj-btc-jsonrpc-gvy/src/main/groovy/org/consensusj/bitcoin/jsonrpc/groovy/BitcoinScriptingClient.groovy +++ b/cj-btc-jsonrpc-gvy/src/main/groovy/org/consensusj/bitcoin/jsonrpc/groovy/BitcoinScriptingClient.groovy @@ -1,5 +1,6 @@ package org.consensusj.bitcoin.jsonrpc.groovy +import com.fasterxml.jackson.databind.JavaType import org.bitcoinj.base.Network import org.consensusj.bitcoin.jsonrpc.BitcoinExtendedClient import org.consensusj.jsonrpc.groovy.DynamicRpcMethodFallback @@ -7,7 +8,7 @@ import org.consensusj.jsonrpc.groovy.DynamicRpcMethodFallback /** * Bitcoin RPC client for scripting. Allows dynamic methods to access new RPCs or RPCs not implemented in Java client */ -class BitcoinScriptingClient extends BitcoinExtendedClient implements DynamicRpcMethodFallback { +class BitcoinScriptingClient extends BitcoinExtendedClient implements DynamicRpcMethodFallback { /** * No args constructor reads bitcoin.conf diff --git a/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinClient.java b/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinClient.java index 6817d5dea..e543f4958 100644 --- a/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinClient.java +++ b/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinClient.java @@ -47,6 +47,7 @@ import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.base.Sha256Hash; import org.bitcoinj.core.Transaction; +import org.consensusj.jsonrpc.JsonRpcTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -152,7 +153,7 @@ public BitcoinClient(SSLContext sslContext, URI server, String rpcuser, String r * @param rpcpassword Password (if required) */ public BitcoinClient(URI server, String rpcuser, String rpcpassword) { - this(getDefaultSSLContext(), (Network) null, server, rpcuser, rpcpassword); + this(JsonRpcTransport.getDefaultSSLContext(), (Network) null, server, rpcuser, rpcpassword); } /** @@ -163,7 +164,7 @@ public BitcoinClient(URI server, String rpcuser, String rpcpassword) { * @param rpcpassword Password (if required) */ public BitcoinClient(Network network, URI server, String rpcuser, String rpcpassword) { - this(getDefaultSSLContext(), network, server, rpcuser, rpcpassword); + this(JsonRpcTransport.getDefaultSSLContext(), network, server, rpcuser, rpcpassword); } @Deprecated diff --git a/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinExtendedClient.java b/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinExtendedClient.java index 565d8232f..0d32745ce 100644 --- a/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinExtendedClient.java +++ b/cj-btc-jsonrpc/src/main/java/org/consensusj/bitcoin/jsonrpc/BitcoinExtendedClient.java @@ -23,6 +23,7 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; +import org.consensusj.jsonrpc.JsonRpcTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,16 +76,16 @@ public BitcoinExtendedClient(SSLContext sslContext, Network network, URI server, } public BitcoinExtendedClient(Network network, URI server, String rpcuser, String rpcpassword) { - this(getDefaultSSLContext(), network, server, rpcuser, rpcpassword); + this(JsonRpcTransport.getDefaultSSLContext(), network, server, rpcuser, rpcpassword); } @Deprecated public BitcoinExtendedClient(NetworkParameters netParams, URI server, String rpcuser, String rpcpassword) { - this(getDefaultSSLContext(), netParams.network(), server, rpcuser, rpcpassword); + this(JsonRpcTransport.getDefaultSSLContext(), netParams.network(), server, rpcuser, rpcpassword); } public BitcoinExtendedClient(URI server, String rpcuser, String rpcpassword) { - this(getDefaultSSLContext(), (Network) null, server, rpcuser, rpcpassword); + this(JsonRpcTransport.getDefaultSSLContext(), (Network) null, server, rpcuser, rpcpassword); } public BitcoinExtendedClient(RpcConfig config) { diff --git a/cj-btc-rx-jsonrpc/src/main/java/org/consensusj/bitcoin/rx/jsonrpc/RxBitcoinClient.java b/cj-btc-rx-jsonrpc/src/main/java/org/consensusj/bitcoin/rx/jsonrpc/RxBitcoinClient.java index 8031ba334..68778b98f 100644 --- a/cj-btc-rx-jsonrpc/src/main/java/org/consensusj/bitcoin/rx/jsonrpc/RxBitcoinClient.java +++ b/cj-btc-rx-jsonrpc/src/main/java/org/consensusj/bitcoin/rx/jsonrpc/RxBitcoinClient.java @@ -7,6 +7,7 @@ import org.consensusj.bitcoin.jsonrpc.BitcoinExtendedClient; import org.consensusj.bitcoin.rx.ChainTipService; import org.consensusj.bitcoin.rx.zeromq.RxBitcoinZmqService; +import org.consensusj.jsonrpc.JsonRpcTransport; import org.consensusj.rx.jsonrpc.RxJsonRpcClient; import javax.net.ssl.SSLContext; @@ -32,7 +33,7 @@ public RxBitcoinClient(Network network, URI server, String rpcuser, String rpcpa } public RxBitcoinClient(Network network, URI server, String rpcuser, String rpcpassword, boolean useZmq) { - this(getDefaultSSLContext(), network, server, rpcuser, rpcpassword, useZmq); + this(JsonRpcTransport.getDefaultSSLContext(), network, server, rpcuser, rpcpassword, useZmq); } public RxBitcoinClient(SSLContext sslContext, Network network, URI server, String rpcuser, String rpcpassword, boolean useZmq) { diff --git a/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcClient.groovy b/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcClient.groovy index 0d6e83413..dd7f4955d 100644 --- a/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcClient.groovy +++ b/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcClient.groovy @@ -3,6 +3,8 @@ package org.consensusj.jsonrpc.groovy import org.consensusj.jsonrpc.JsonRpcMessage import org.consensusj.jsonrpc.JsonRpcClientHttpUrlConnection +import com.fasterxml.jackson.databind.JavaType; + /** * Client that uses Groovy methodMissing to allow any JSON-RPC call to be made as client.rpcMethod(args). * Note that calling a non-existent method will result in an error from the server. @@ -14,7 +16,7 @@ import org.consensusj.jsonrpc.JsonRpcClientHttpUrlConnection * This client and the {@link DynamicRpcMethodFallback} trait are provided for those looking for something simple, * flexible, dynamic, and Groovy. */ -class DynamicRpcClient extends JsonRpcClientHttpUrlConnection implements DynamicRpcMethodFallback { +class DynamicRpcClient extends JsonRpcClientHttpUrlConnection implements DynamicRpcMethodFallback { DynamicRpcClient(URI server, String rpcuser, String rpcpassword) { this(JsonRpcMessage.Version.V2, server, rpcuser, rpcpassword) diff --git a/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcMethodFallback.groovy b/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcMethodFallback.groovy index bfc034890..27e48a2a8 100644 --- a/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcMethodFallback.groovy +++ b/consensusj-jsonrpc-gvy/src/main/groovy/org/consensusj/jsonrpc/groovy/DynamicRpcMethodFallback.groovy @@ -28,7 +28,7 @@ import java.util.concurrent.CompletableFuture * @see Groovy Language Documentation: methodMissing * @see Groovy Language Documentation: Implementing a trait at runtime */ -trait DynamicRpcMethodFallback implements JsonRpcClient { +trait DynamicRpcMethodFallback implements JsonRpcClient { /** * Dynamically forward missing method calls to the server and return a result. * diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/AbstractRpcClient.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/AbstractRpcClient.java index bbf3f5aad..50e3a4525 100644 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/AbstractRpcClient.java +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/AbstractRpcClient.java @@ -2,11 +2,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import javax.net.ssl.SSLContext; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; // TODO: Rather than implementing transport (HttpUrlConnection vs. java.net.http) with subclasses use composition // In other words, the constructor should take a transport implementation object. @@ -39,13 +39,17 @@ // map from request to string/stream and to map from string/stream to response. The java.net.http implementation has already defined // some functional interfaces for this, so coming up with an interface that both the java.net.http implementation and the HttpUrlConnection // implementation can use will lead to this "SECOND STEP" +// +// Update: Now that JsonRpcClient is a generic with , we have loosened the Jackson coupling somewhat. The sendRequestForResponse +// and sendRequestForResponseAsync methods from JsonRpcClient have been moved to the JsonRpcTransport class which the JavaNet and HttpUrlConnection +// flavors implement. The AbstractRpcClient constructor should be passed an instance of either transport class and forward methods calls for +// sendRequestForResponseAsync (and sendRequestForResponse ?) to the transport. /** - * Abstract Base class for a strongly-typed, Jackson-based JSON-RPC client. Most of the work is done - * in default methods in {@link JacksonRpcClient} This abstract class implements the constructors, static fields, and + * Abstract Base class for a strongly-typed, Jackson-based JSON-RPC client. This abstract class implements the constructors, static fields, and * getters, but leaves the core {@code sendRequestForResponse} method as {@code abstract} to be implemented by subclasses * allowing implementation with alternative HTTP client libraries. */ -public abstract class AbstractRpcClient implements JacksonRpcClient { +public abstract class AbstractRpcClient implements JsonRpcClient { /** * The default JSON-RPC version in JsonRpcRequest is now '2.0', but since most * requests are created inside {@code RpcClient} subclasses, we'll continue to default @@ -70,37 +74,39 @@ public JsonRpcMessage.Version getJsonRpcVersion() { return jsonRpcVersion; } - @Override + /** + * Convenience method for requesting an asynchronous response with a {@link JsonNode} for the result. + * @param request The request to send + * @return A future JSON RPC Response with `result` of type {@code JsonNode} + */ + public CompletableFuture> sendRequestForResponseAsync(JsonRpcRequest request) { + return sendRequestForResponseAsync(request, responseTypeFor(JsonNode.class)); + } + public ObjectMapper getMapper() { return mapper; } @Override - public JavaType getDefaultType() { + public JavaType defaultType() { return defaultType; } - /** - * Return the default {@link SSLContext} without declaring a checked exception - * @return The default {@code SSLContext} - */ - protected static SSLContext getDefaultSSLContext() { - try { - return SSLContext.getDefault(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } + @Override + public JavaType responseTypeFor(JavaType resultType) { + return getMapper().getTypeFactory(). + constructParametricType(JsonRpcResponse.class, resultType); } - /** - * Encode username password as Base64 for basic authentication - *

- * We're using {@link java.util.Base64}, which requires Android 8.0 or later. - * - * @param authString An authorization string of the form `username:password` - * @return A compliant Base64 encoding of `authString` - */ - protected static String base64Encode(String authString) { - return Base64.getEncoder().encodeToString(authString.getBytes()).trim(); + @Override + public JavaType responseTypeFor(Class resultType) { + return getMapper().getTypeFactory(). + constructParametricType(JsonRpcResponse.class, resultType); } + + @Override + public JavaType typeForClass(Class clazz) { + return getMapper().constructType(clazz); + } + } diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JacksonRpcClient.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JacksonRpcClient.java deleted file mode 100644 index eede9464b..000000000 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JacksonRpcClient.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.consensusj.jsonrpc; - -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Supplier; - -// TODO: Step 1: Create interface in JsonRpcClient replacing JavaType with java.lang.reflect.Type -// TODO: Step 2: Eliminate JacksonRpcClient by pulling up or pushing down all methods -// TODO: Step 3: Migrate JSON mapping function to component "JsonRpcTypeProcessor" class with Jackson implementation -/** - * Interface with default methods for a strongly-typed JSON-RPC client that uses Jackson to map from JSON to Java Objects. - */ -public interface JacksonRpcClient extends JsonRpcClient { - - ObjectMapper getMapper(); - - JavaType getDefaultType(); - - default JavaType responseTypeFor(JavaType resultType) { - return getMapper().getTypeFactory(). - constructParametricType(JsonRpcResponse.class, resultType); - } - - default JavaType responseTypeFor(Class resultType) { - return getMapper().getTypeFactory(). - constructParametricType(JsonRpcResponse.class, resultType); - } - - default JavaType typeForClass(Class clazz) { - return getMapper().constructType(clazz); - } - - /** - * Send a {@link JsonRpcRequest} for a {@link JsonRpcResponse} - *

Synchronous subclasses should override this method to prevent {@link CompletableFuture#supplyAsync(Supplier)} from - * being called twice when {@link AsyncSupport} is being used. Eventually we'll migrate more of the codebase to native - * async, and then we won't have to worry about calling {@code supplyAsync} twice. - * @param Type of result object - * @param request The request to send - * @param responseType The response type expected (used by Jackson for conversion) - * @return A JSON RPC Response with `result` of type `R` - * @throws IOException network error - * @throws JsonRpcStatusException JSON RPC status error - */ - default JsonRpcResponse sendRequestForResponse(JsonRpcRequest request, JavaType responseType) throws IOException, JsonRpcStatusException { - return syncGet(sendRequestForResponseAsync(request, responseType)); - } - - /** - * Send a {@link JsonRpcRequest} for a {@link JsonRpcResponse} asynchronously. - * @param Type of result object - * @param request The request to send - * @param responseType The response type expected (used by Jackson for conversion) - * @return A future JSON RPC Response with `result` of type `R` - */ - CompletableFuture> sendRequestForResponseAsync(JsonRpcRequest request, JavaType responseType); - - /** - * Convenience method for requesting a response with a {@link JsonNode} for the result. - * @param request The request to send - * @return A JSON RPC Response with `result` of type {@code JsonNode} - * @throws IOException network error - * @throws JsonRpcStatusException JSON RPC status error - */ - default JsonRpcResponse sendRequestForResponse(JsonRpcRequest request) throws IOException, JsonRpcStatusException { - return syncGet(sendRequestForResponseAsync(request)); - } - - /** - * Convenience method for requesting an asynchronous response with a {@link JsonNode} for the result. - * @param request The request to send - * @return A future JSON RPC Response with `result` of type {@code JsonNode} - */ - default CompletableFuture> sendRequestForResponseAsync(JsonRpcRequest request) { - return sendRequestForResponseAsync(request, responseTypeFor(JsonNode.class)); - } - - private CompletableFuture sendRequestForResultAsync(JsonRpcRequest request, JavaType resultType) { - CompletableFuture> responseFuture = sendRequestForResponseAsync(request, responseTypeFor(resultType)); - -// assert response != null; -// assert response.getJsonrpc() != null; -// assert response.getJsonrpc().equals("2.0"); -// assert response.getId() != null; -// assert response.getId().equals(request.getId()); - - // TODO: Error case should probably complete with JsonRpcErrorException (not status exception with code 200) - return responseFuture.thenCompose(resp -> (resp.getError() == null || resp.getError().getCode() == 0) - ? CompletableFuture.completedFuture(resp.getResult()) - : CompletableFuture.failedFuture(new JsonRpcStatusException( - resp.getError().getMessage(), - 200, // If response code wasn't 200 we couldn't be here - null, - resp.getError().getCode(), - null, - resp)) - ); - } - - /** - * JSON-RPC remote method call that returns 'response.result` - * - * @param Type of result object - * @param method JSON RPC method call to send - * @param resultType desired result type as a Java class object - * @param params JSON RPC params - * @return the 'response.result' field of the JSON RPC response converted to type R - */ - @Override - default R send(String method, Class resultType, List params) throws IOException, JsonRpcStatusException { - return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), typeForClass(resultType))); - } - - default CompletableFuture sendAsync(String method, Class resultType, List params) { - return sendRequestForResultAsync(buildJsonRequest(method, params), typeForClass(resultType)); - } - - default CompletableFuture sendAsync(String method, JavaType resultType, List params) { - return sendRequestForResultAsync(buildJsonRequest(method, params), resultType); - } - - /** - * JSON-RPC remote method call that returns {@code response.result} - * - * @param Type of result object - * @param method JSON RPC method call to send - * @param resultType desired result type as a Jackson JavaType object - * @param params JSON RPC params - * @return the 'response.result' field of the JSON RPC response converted to type R - */ - default R send(String method, JavaType resultType, List params) throws IOException, JsonRpcStatusException { - return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), resultType)); - } - - /** - * Varargs version - */ - default R send(String method, JavaType resultType, Object... params) throws IOException, JsonRpcStatusException { - return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), resultType)); - } - - default CompletableFuture sendAsync(String method, JavaType resultType, Object... params) { - return sendRequestForResultAsync(buildJsonRequest(method, params), resultType); - } - - /** - * Call an RPC method and return default object type. - *

- * Caller should cast returned object to the correct type. - *

- * Useful for: - *

    - *
  • Dynamically-dispatched JSON-RPC methods calls via Groovy subclasses
  • - *
  • Simple (not client-side validated) command line utilities
  • - *
  • Functional tests that need to send incorrect types to the server to test error handling
  • - *
- * - * @param Type of result object - * @param method JSON RPC method call to send - * @param params JSON RPC parameters as a `List` - * @return the 'response.result' field of the JSON RPC response cast to type R - * @throws IOException network error - * @throws JsonRpcStatusException JSON RPC status error - */ - @Override - default R send(String method, List params) throws IOException, JsonRpcStatusException { - return send(method, getDefaultType(), params); - } - - @Override - default CompletableFuture sendAsync(String method, List params) { - return sendAsync(method, getDefaultType(), params); - } -} diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClient.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClient.java index c86328c7c..80e577d26 100644 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClient.java +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClient.java @@ -1,17 +1,16 @@ package org.consensusj.jsonrpc; import java.io.IOException; +import java.lang.reflect.Type; import java.net.HttpURLConnection; -import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; /** * JSON-RPC client interface. This interface is independent of the JSON conversion library * (the default implementation uses Jackson) and HTTP client library (currently {@link HttpURLConnection}). - * For historical reasons the interface is synchronous, but {@link AsyncSupport} makes it easier + * For historical reasons the interface is mostly synchronous, but {@link AsyncSupport} makes it easier * to add use of {@link java.util.concurrent.CompletableFuture} for special cases. In the future * this interface may change to natively asynchronous. *

@@ -20,7 +19,7 @@ * @see JSON-RPC 1.0 Specification (2005) * @see JSON-RPC 2.0 Specification */ -public interface JsonRpcClient extends AutoCloseable, AsyncSupport { +public interface JsonRpcClient extends JsonRpcTransport, AutoCloseable { /** * Return the JSON-RPC version used by the implementation @@ -30,32 +29,31 @@ public interface JsonRpcClient extends AutoCloseable, AsyncSupport { JsonRpcMessage.Version getJsonRpcVersion(); /** - * Get the URI of the remote server - * @return URI of remote server - */ - URI getServerURI(); - - /** - * Call an RPC method and return "default" object type. Caller should cast returned object to the correct type. + * Call an RPC method and return default object type. *

- * The parameter list is "untyped" (declared as {@code List}) and implementations are responsible - * for converting each Java object parameter to a valid and correctly-typed (for {@code method}) JSON object. + * Caller should cast returned object to the correct type. *

- * This is used to implement the {@code DynamicRpcMethodFallback} trait in Groovy which is applied - * to various Groovy RPC client implementations that typically inherit statically-dispatched - * methods from Java classes, but use {@code methodMissing()} to add JSON-RPC methods dynamically. - * This may be useful in other Dynamic JVM languages, as well. + * Useful for: + *

    + *
  • Dynamically-dispatched JSON-RPC methods calls via Groovy subclasses
  • + *
  • Simple (not client-side validated) command line utilities
  • + *
  • Functional tests that need to send incorrect types to the server to test error handling
  • + *
* - * @param method JSON RPC method call to send - * @param params JSON RPC parameters using types that are convertible to JSON * @param Type of result object - * @return the `response.result` field of the JSON-RPC response cast to type R + * @param method JSON RPC method call to send + * @param params JSON RPC parameters as a `List` + * @return the 'response.result' field of the JSON RPC response cast to type R * @throws IOException network error * @throws JsonRpcStatusException JSON RPC status error */ - R send(String method, List params) throws IOException, JsonRpcStatusException; + default R send(String method, List params) throws IOException, JsonRpcStatusException { + return send(method, defaultType(), params); + } - CompletableFuture sendAsync(String method, List params); + default CompletableFuture sendAsync(String method, List params) { + return sendAsync(method, defaultType(), params); + } /** * Call an RPC method and return default object type. @@ -73,10 +71,6 @@ default R send(String method, Object... params) throws IOException, JsonRpcS return send(method, Arrays.asList(params)); } - R send(String method, Class resultType, List params) throws IOException, JsonRpcStatusException; - - CompletableFuture sendAsync(String method, Class resultType, List params); - default R send(String method, Class resultType, Object... params) throws IOException, JsonRpcStatusException { return send(method, resultType, Arrays.asList(params)); } @@ -86,30 +80,70 @@ default CompletableFuture sendAsync(String method, Class resultType, O } /** - * Synchronously complete a JSON-RPC request by calling {@link CompletableFuture#get()}, unwrapping nested - * {@link JsonRpcException} or {@link IOException} from {@link ExecutionException}. - * @param future The {@code CompletableFuture} (result of JSON-RPC request) to unwrap - * @return A JSON-RPC result - * @param The expected result type - * @throws IOException If {@link CompletableFuture#get} threw {@code ExecutionException} caused by {@code IOException} - * @throws JsonRpcException If {@link CompletableFuture#get} threw {@code ExecutionException} caused by {@code JsonRpcException} - * @throws RuntimeException If {@link CompletableFuture#get} threw {@link InterruptedException} or other {@link ExecutionException}. + * JSON-RPC remote method call that returns 'response.result` + * + * @param Type of result object + * @param method JSON RPC method call to send + * @param resultType desired result type as a Java class object + * @param params JSON RPC params + * @return the 'response.result' field of the JSON RPC response converted to type R */ - default R syncGet(CompletableFuture future) throws IOException, JsonRpcException { - try { - return future.get(); - } catch (InterruptedException ie) { - throw new RuntimeException(ie); - } catch (ExecutionException ee) { - Throwable cause = ee.getCause(); - if (cause instanceof JsonRpcException) { - throw (JsonRpcException) cause; - } else if (cause instanceof IOException) { - throw (IOException) cause; - } else { - throw new RuntimeException(ee); - } - } + default R send(String method, Class resultType, List params) throws IOException, JsonRpcStatusException { + return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), typeForClass(resultType))); + } + + default CompletableFuture sendAsync(String method, Class resultType, List params) { + return sendRequestForResultAsync(buildJsonRequest(method, params), typeForClass(resultType)); + } + + default CompletableFuture sendAsync(String method, T resultType, List params) { + return sendRequestForResultAsync(buildJsonRequest(method, params), resultType); + } + + /** + * JSON-RPC remote method call that returns {@code response.result} + * + * @param Type of result object + * @param method JSON RPC method call to send + * @param resultType desired result type as a Jackson JavaType object + * @param params JSON RPC params + * @return the 'response.result' field of the JSON RPC response converted to type R + */ + default R send(String method, T resultType, List params) throws IOException, JsonRpcStatusException { + return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), resultType)); + } + + /** + * Varargs version + */ + default R send(String method, T resultType, Object... params) throws IOException, JsonRpcStatusException { + return syncGet(sendRequestForResultAsync(buildJsonRequest(method, params), resultType)); + } + + default CompletableFuture sendAsync(String method, T resultType, Object... params) { + return sendRequestForResultAsync(buildJsonRequest(method, params), resultType); + } + + private CompletableFuture sendRequestForResultAsync(JsonRpcRequest request, T resultType) { + CompletableFuture> responseFuture = sendRequestForResponseAsync(request, responseTypeFor(resultType)); + +// assert response != null; +// assert response.getJsonrpc() != null; +// assert response.getJsonrpc().equals("2.0"); +// assert response.getId() != null; +// assert response.getId().equals(request.getId()); + + // TODO: Error case should probably complete with JsonRpcErrorException (not status exception with code 200) + return responseFuture.thenCompose(resp -> (resp.getError() == null || resp.getError().getCode() == 0) + ? CompletableFuture.completedFuture(resp.getResult()) + : CompletableFuture.failedFuture(new JsonRpcStatusException( + resp.getError().getMessage(), + 200, // If response code wasn't 200 we couldn't be here + null, + resp.getError().getCode(), + null, + resp)) + ); } /** @@ -136,4 +170,11 @@ default JsonRpcRequest buildJsonRequest(String method, Object... params) { @Override default void close() throws Exception { } + + T defaultType(); + + T responseTypeFor(T resultType); + T responseTypeFor(Class resultType); + + T typeForClass(Class clazz); } diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnection.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnection.java index 2ecf017d8..fed428ecc 100644 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnection.java +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnection.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -21,7 +22,7 @@ * JSON-RPC Client using {@link HttpURLConnection} formerly named{@code RpcClient}. *

* This is a concrete class with generic JSON-RPC functionality, it implements the abstract - * method {@link AbstractRpcClient#sendRequestForResponseAsync(JsonRpcRequest, JavaType)} using {@link HttpURLConnection}. + * method {@link JsonRpcClient#sendRequestForResponseAsync(JsonRpcRequest, Type)} using {@link HttpURLConnection}. *

* Uses strongly-typed POJOs representing {@link JsonRpcRequest} and {@link JsonRpcResponse}. The * response object uses a parameterized type for the object that is the actual JSON-RPC `result`. @@ -55,7 +56,7 @@ public JsonRpcClientHttpUrlConnection(SSLContext sslContext, JsonRpcMessage.Vers * @param rpcPassword password for the RPC HTTP connection */ public JsonRpcClientHttpUrlConnection(JsonRpcMessage.Version jsonRpcVersion, URI server, final String rpcUser, final String rpcPassword) { - this(getDefaultSSLContext(), jsonRpcVersion, server, rpcUser, rpcPassword); + this(JsonRpcTransport.getDefaultSSLContext(), jsonRpcVersion, server, rpcUser, rpcPassword); } /** @@ -191,7 +192,7 @@ private HttpURLConnection openConnection() throws IOException { connection.setRequestProperty("Connection", "close"); // Avoid EOFException: http://stackoverflow.com/questions/19641374/android-eofexception-when-using-httpurlconnection-headers String auth = username + ":" + password; - String basicAuth = "Basic " + base64Encode(auth); + String basicAuth = "Basic " + JsonRpcTransport.base64Encode(auth); connection.setRequestProperty ("Authorization", basicAuth); return connection; diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientJavaNet.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientJavaNet.java index 3fcfd9fd9..34463199a 100644 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientJavaNet.java +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/JsonRpcClientJavaNet.java @@ -29,7 +29,7 @@ public class JsonRpcClientJavaNet extends AbstractRpcClient { public JsonRpcClientJavaNet(JsonRpcMessage.Version jsonRpcVersion, URI server, final String rpcUser, final String rpcPassword) { - this(getDefaultSSLContext(), jsonRpcVersion, server, rpcUser, rpcPassword); + this(JsonRpcTransport.getDefaultSSLContext(), jsonRpcVersion, server, rpcUser, rpcPassword); } public JsonRpcClientJavaNet(SSLContext sslContext, JsonRpcMessage.Version jsonRpcVersion, URI server, final String rpcUser, final String rpcPassword) { @@ -118,7 +118,7 @@ private HttpRequest buildJsonRpcPostRequest(String requestString) { log.info("request is: {}", requestString); String auth = username + ":" + password; - String basicAuth = "Basic " + base64Encode(auth); + String basicAuth = "Basic " + JsonRpcTransport.base64Encode(auth); return HttpRequest .newBuilder(serverURI) diff --git a/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnectionSpec.groovy b/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnectionSpec.groovy index 3cfa8eec9..c749d2680 100644 --- a/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnectionSpec.groovy +++ b/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcClientHttpUrlConnectionSpec.groovy @@ -22,7 +22,7 @@ class JsonRpcClientHttpUrlConnectionSpec extends Specification { @Unroll def "Base64 works for #input"(String input, String expectedResult) { expect: - expectedResult == JsonRpcClientHttpUrlConnection.base64Encode(input) + expectedResult == JsonRpcTransport.base64Encode(input) where: input | expectedResult diff --git a/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/AbstractRpcClientSpec.groovy b/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcTransportSpec.groovy similarity index 83% rename from consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/AbstractRpcClientSpec.groovy rename to consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcTransportSpec.groovy index 8865f83ca..33d5128f3 100644 --- a/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/AbstractRpcClientSpec.groovy +++ b/consensusj-jsonrpc/src/test/groovy/org/consensusj/jsonrpc/JsonRpcTransportSpec.groovy @@ -6,7 +6,7 @@ import spock.lang.Unroll /** * Basic test of our copied Base64 class. */ -class AbstractRpcClientSpec extends Specification { +class JsonRpcTransportSpec extends Specification { @Unroll def "Base64 Basic Auth Test #myInt" (myInt, expectedResult) { @@ -14,7 +14,7 @@ class AbstractRpcClientSpec extends Specification { def auth = "myuser" + ":" + "mypass" + myInt; when: - def basicAuth = "Basic " + AbstractRpcClient.base64Encode(auth); + def basicAuth = "Basic " + JsonRpcTransport.base64Encode(auth); then: basicAuth == expectedResult