001package org.consensusj.jsonrpc; 002 003import com.fasterxml.jackson.core.JsonProcessingException; 004import com.fasterxml.jackson.databind.JavaType; 005import com.fasterxml.jackson.databind.ObjectMapper; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009import javax.net.ssl.SSLContext; 010import java.lang.reflect.Type; 011import java.net.URI; 012import java.net.http.HttpClient; 013import java.net.http.HttpRequest; 014import java.net.http.HttpResponse; 015import java.nio.charset.StandardCharsets; 016import java.time.Duration; 017import java.util.Optional; 018import java.util.concurrent.CompletableFuture; 019import java.util.concurrent.CompletionException; 020import java.util.function.Function; 021 022/** 023 * Incubating JSON-RPC client using {@link java.net.http.HttpClient} 024 */ 025public class JsonRpcClientJavaNet implements JsonRpcTransport<JavaType> { 026 private static final Logger log = LoggerFactory.getLogger(JsonRpcClientJavaNet.class); 027 028 private final ObjectMapper mapper; 029 private final URI serverURI; 030 private final String username; 031 private final String password; 032 private final HttpClient client; 033 private static final String UTF8 = StandardCharsets.UTF_8.name(); 034 035 036 public JsonRpcClientJavaNet(ObjectMapper mapper, URI server, final String rpcUser, final String rpcPassword) { 037 this(mapper, JsonRpcTransport.getDefaultSSLContext(), server, rpcUser, rpcPassword); 038 } 039 040 public JsonRpcClientJavaNet(ObjectMapper mapper, SSLContext sslContext, URI server, final String rpcUser, final String rpcPassword) { 041 log.debug("Constructing JSON-RPC client for: {}", server); 042 this.mapper = mapper; 043 this.serverURI = server; 044 this.username = rpcUser; 045 this.password = rpcPassword; 046 this.client = HttpClient.newBuilder() 047 .connectTimeout(Duration.ofMinutes(2)) 048 .sslContext(sslContext) 049 .build(); 050 } 051 052 /** 053 * {@inheritDoc} 054 */ 055 @Override 056 public <R> CompletableFuture<JsonRpcResponse<R>> sendRequestForResponseAsync(JsonRpcRequest request, JavaType responseType) { 057 return sendCommon(request) 058 .thenApply(mappingFuncFor(responseType)); 059 } 060 061 // For testing only 062 CompletableFuture<String> sendRequestForResponseString(JsonRpcRequest request) { 063 return sendCommon(request); 064 } 065 066 /** 067 * {@inheritDoc} 068 */ 069 @Override 070 public URI getServerURI() { 071 return serverURI; 072 } 073 074 private String encodeJsonRpcRequest(JsonRpcRequest request) throws JsonProcessingException { 075 return mapper.writeValueAsString(request); 076 } 077 078 /** 079 * @param request A JSON-RPC request 080 * @return A future for a JSON-RPC response in String format 081 */ 082 private CompletableFuture<String> sendCommon(JsonRpcRequest request) { 083 log.debug("Send: {}", request); 084 try { 085 HttpRequest httpRequest = buildJsonRpcPostRequest(request); 086 return client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) 087 .whenComplete(this::log) 088 .thenCompose(this::handleStatusError) 089 .thenApply(HttpResponse::body) 090 .whenComplete(this::log); 091 } catch (JsonProcessingException e) { 092 return CompletableFuture.failedFuture(e); 093 } 094 } 095 096 /** 097 * Process the HTTP status code. Treats values other than 200 as an error. 098 * @param response A received HTTP response. 099 * @return Completed or failed future as appropriate. 100 */ 101 private CompletableFuture<HttpResponse<String>> handleStatusError(HttpResponse<String> response) { 102 if (response.statusCode() != 200) { 103 String body = response.body(); 104 log.warn("Bad status code: {}: {}", response.statusCode(), body); 105 log.debug("Headers: {}", response.headers()); 106 // Return a failed future containing a JsonRpcStatusException. Create the exception 107 // from a JsonRpcResponse if one can be built, otherwise just use the body string. 108 return CompletableFuture.failedFuture(response.headers() 109 .firstValue("Content-Type") 110 .map(s -> s.contains("application/json")) 111 .flatMap(b -> readErrorResponse(body)) 112 .map(r -> new JsonRpcStatusException(response.statusCode(), r)) 113 .orElse(new JsonRpcStatusException(response.statusCode(), body)) 114 ); 115 } else { 116 return CompletableFuture.completedFuture(response); 117 } 118 } 119 120 // Try to read a JsonRpcResponse from a string (error case) 121 private Optional<JsonRpcResponse<Object>> readErrorResponse(String body) { 122 JsonRpcResponse<Object> response; 123 try { 124 response = mapper.readValue(body, responseTypeFor(Object.class)); 125 } catch (JsonProcessingException e) { 126 response = null; 127 } 128 return Optional.ofNullable(response); 129 } 130 131 private HttpRequest buildJsonRpcPostRequest(JsonRpcRequest request) throws JsonProcessingException { 132 String requestString = encodeJsonRpcRequest(request); 133 return buildJsonRpcPostRequest(requestString); 134 } 135 136 private HttpRequest buildJsonRpcPostRequest(String requestString) { 137 log.info("request is: {}", requestString); 138 139 String auth = username + ":" + password; 140 String basicAuth = "Basic " + JsonRpcTransport.base64Encode(auth); 141 142 return HttpRequest 143 .newBuilder(serverURI) 144 .header("Content-Type", "application/json;charset=" + UTF8) 145 .header("Accept-Charset", UTF8) 146 .header("Accept", "application/json") 147 .header("Authorization", basicAuth) 148 .POST(HttpRequest.BodyPublishers.ofString(requestString)) 149 .build(); 150 } 151 152 // return a MappingFunction for a given type 153 private <R, T extends Type> MappingFunction<R> mappingFuncFor(T responseType) { 154 return s -> mapper.readValue(s, (JavaType) responseType); 155 } 156 157 /** 158 * Map a response string to a Java object. Wraps checked {@link JsonProcessingException} 159 * in unchecked {@link CompletionException}. 160 * @param <R> result type 161 */ 162 @FunctionalInterface 163 protected interface MappingFunction<R> extends Function<String, R> { 164 165 /** 166 * Gets a result. Wraps checked {@link JsonProcessingException} in {@link CompletionException} 167 * @param s input 168 * @return a result 169 * @throws CompletionException (unchecked) if a JsonProcessingException exception occurs 170 */ 171 @Override 172 default R apply(String s) throws CompletionException { 173 try { 174 return applyThrows(s); 175 } catch (Exception e) { 176 throw new CompletionException(e); 177 } 178 } 179 180 /** 181 * Gets a result and may throw a checked exception. 182 * @param s input 183 * @return a result 184 * @throws JsonProcessingException Checked Exception 185 */ 186 R applyThrows(String s) throws Exception; 187 } 188 189 /** 190 * Logging action for a {@code CompletionStage} that returns {@link HttpResponse} 191 * <p> 192 * Note that an error at this layer should be treated as a warning for logging purposes, because network 193 * errors are relatively common and should be handled and/or logged at higher layers of the stack. 194 * @param httpResponse non-null on success 195 * @param t non-null on error 196 */ 197 private void log(HttpResponse<String> httpResponse, Throwable t) { 198 if ((httpResponse != null)) { 199 log.info("log data string: {}", httpResponse); 200 } else { 201 log.warn("exception: ", t); 202 } 203 } 204 205 /** 206 * Logging action for a {@code CompletionStage} that returns {@link String}. In the current 207 * implementation of this client, that should be a JSON-formatted string. 208 * <p> 209 * Note that an error at this layer should be treated as a warning for logging purposes, because network 210 * errors are relatively common and should be handled and/or logged at higher layers of the stack. 211 * @param s non-null on success 212 * @param t non-null on error 213 */ 214 private void log(String s, Throwable t) { 215 if ((s != null)) { 216 log.info("log data string: {}", s.substring(0 ,Math.min(100, s.length()))); 217 } else { 218 log.warn("exception: ", t); 219 } 220 } 221 222 private JavaType responseTypeFor(Class<?> resultType) { 223 return mapper.getTypeFactory(). 224 constructParametricType(JsonRpcResponse.class, resultType); 225 } 226}