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.ObjectNode;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.DefaultConnectionReuseStrategy;
import org.apache.http.impl.nio.DefaultHttpClientIODispatch;
import org.apache.http.impl.nio.pool.BasicNIOConnFactory;
import org.apache.http.impl.nio.pool.BasicNIOConnPool;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.message.BasicHttpEntityEnclosingRequest;
import org.apache.http.nio.protocol.BasicAsyncRequestProducer;
import org.apache.http.nio.protocol.BasicAsyncResponseConsumer;
import org.apache.http.nio.protocol.HttpAsyncRequestExecutor;
import org.apache.http.nio.protocol.HttpAsyncRequester;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOEventDispatch;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.ImmutableHttpProcessor;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.RequestContent;
import org.apache.http.protocol.RequestExpectContinue;
import org.apache.http.protocol.RequestTargetHost;
import org.apache.http.protocol.RequestUserAgent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
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;
/**
* Implements an asynchronous JSON-RPC 2.0 HTTP client. This class has a
* dependency on Apache Commons Codec, Apache
* <p>
* Because this implementation uses an HTTP request pool, timeouts are
* controlled at a global level, rather than per-request.
* <p>
* The following JVM system properties control the behavior of the pool:
* <ul>
* <li>com.googlecode.jsonrpc4j.async.socket.timeout - overall socket idle
* (keep-alive) timeout in milliseconds, default is 30 seconds</li>
* <li>com.googlecode.jsonrpc4j.async.connect.timeout - socket connect timeout
* in milliseconds, default is 30 seconds</li>
* <li>com.googlecode.jsonrpc4j.async.socket.buffer - socket buffer size in
* bytes, default is 8Kb (8192 bytes)</li>
* <li>com.googlecode.jsonrpc4j.async.tcp.nodelay - true to use TCP_NODELAY,
* false to disable, default is true
* <li>com.googlecode.jsonrpc4j.async.max.inflight.route - maximum number of
* in-flight requests per route (unique URL, minus query string), default is 500
* </li>
* <li>com.googlecode.jsonrpc4j.async.max.inflight.total - maximum number of
* total in-flight requests (across all providers), default is 500</li>
* <li>com.googlecode.jsonrpc4j.async.reactor.threads - number of asynchronous
* IO reactor threads, default is 2 (more than sufficient for most clients)</li>
* </ul>
*
* @author Brett Wooldridge
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class JsonRpcHttpAsyncClient {
private static final Logger logger = LoggerFactory.getLogger(JsonRpcHttpAsyncClient.class);
private static final AtomicBoolean initialized = new AtomicBoolean();
private static final AtomicLong nextId = new AtomicLong();
private static HttpAsyncRequester requester;
private static BasicNIOConnPool pool;
private static SSLContext sslContext;
private final ExceptionResolver exceptionResolver;
private final Map<String, String> headers = new HashMap<>();
private final ObjectMapper mapper;
private final URL serviceUrl;
{
initialize();
}
/**
* Creates the {@link JsonRpcHttpAsyncClient} bound to the given {@code serviceUrl}.
*
* @param serviceUrl the service end-point URL
*/
public JsonRpcHttpAsyncClient(URL serviceUrl) {
this(new ObjectMapper(), serviceUrl, new HashMap<String, String>());
}
/**
* Creates the {@link JsonRpcHttpAsyncClient} using the specified {@code ObjectMapper} and bound to the given
* {@code serviceUrl}. The headers provided in the {@code headers} map are added to every request
* made to the {@code serviceUrl}.
*
* @param mapper the {@link ObjectMapper} to use for json<->java conversion
* @param serviceUrl the service end-point URL
* @param headers the headers
*/
public JsonRpcHttpAsyncClient(ObjectMapper mapper, URL serviceUrl, Map<String, String> headers) {
this(mapper, DefaultExceptionResolver.INSTANCE, serviceUrl, headers);
}
/**
* Creates the {@link JsonRpcHttpAsyncClient} using the specified
* {@link ObjectMapper} and {@link ExceptionResolver}, bound to the given
* {@code serviceUrl}. The headers provided in the {@code headers} map are
* added to every request made to the {@code serviceUrl}.
* The {@link ExceptionResolver} can not be null.
*
* @param mapper the {@link ObjectMapper} to use for json<->java conversion
* @param exceptionResolver the {@link ExceptionResolver} translating remote exceptions.
* @param serviceUrl the service end-point URL
* @param headers the headers
*/
public JsonRpcHttpAsyncClient(ObjectMapper mapper, ExceptionResolver exceptionResolver, URL serviceUrl, Map<String, String> headers) {
this.mapper = mapper;
this.serviceUrl = serviceUrl;
this.headers.putAll(headers);
this.exceptionResolver = exceptionResolver;
if(this.exceptionResolver == null) {
throw new IllegalArgumentException("ExceptionResolver can not be null");
}
}
/**
* Creates the {@link JsonRpcHttpAsyncClient} bound to the given
* {@code serviceUrl}. The headers provided in the {@code headers} map are
* added to every request made to the {@code serviceUrl}.
*
* @param serviceUrl the service end-point URL
* @param headers the headers
*/
public JsonRpcHttpAsyncClient(URL serviceUrl, Map<String, String> headers) {
this(new ObjectMapper(), serviceUrl, headers);
}
/**
* Set the SSLContext to be used to create SSL connections. This method most
* be called before the first {@code JsonRpcHttpAsyncClient} is constructed,
* otherwise it has no effect.
*
* @param sslContext the {@code SSLContext to use}
*/
public static void setSSLContext(SSLContext sslContext) {
JsonRpcHttpAsyncClient.sslContext = sslContext;
}
/**
* Invokes the given method with the given arguments and returns
* immediately. The {@code Future} object that is returned can be used to
* retrieve the result.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @return the response {@code Future<T>}
*/
public Future<Object> invoke(String methodName, Object argument) {
return invoke(methodName, argument, Object.class, new HashMap<String, String>());
}
/**
* Invokes the given method with the given arguments and returns
* immediately. The {@code extraHeaders} are added to the request. The
* {@code Future<T>} object that is returned can be used to retrieve the
* result.
*
* @param methodName the name of the method to invoke
* @param argument the argument to the method
* @param returnType the return type
* @param extraHeaders extra headers to add to the request
* @param <T> the return type
* @return the response {@code Future<T>}
*/
private <T> Future<T> invoke(String methodName, Object argument, Class<T> returnType, Map<String, String> extraHeaders) {
return doInvoke(methodName, argument, returnType, extraHeaders, new JsonRpcFuture<T>());
}
/**
* Invokes the given method with the given arguments and invokes the
* {@code JsonRpcCallback} with the result cast to the given
* {@code returnType}, or null if void. The {@code extraHeaders} are added
* to the request.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @param extraHeaders extra headers to add to the request
* @param returnType the return type
* @param callback the {@code JsonRpcCallback}
*/
@SuppressWarnings("unchecked")
private <T> Future<T> doInvoke(String methodName, Object argument, Class<T> returnType, Map<String, String> extraHeaders, JsonRpcCallback<T> callback) {
String path = serviceUrl.getPath() + (serviceUrl.getQuery() != null ? "?" + serviceUrl.getQuery() : "");
int port = serviceUrl.getPort() != -1 ? serviceUrl.getPort() : serviceUrl.getDefaultPort();
HttpRequest request = new BasicHttpEntityEnclosingRequest("POST", path);
addHeaders(request, headers);
addHeaders(request, extraHeaders);
try {
writeRequest(methodName, argument, request);
} catch (IOException e) {
callback.onError(e);
}
HttpHost target = new HttpHost(serviceUrl.getHost(), port, serviceUrl.getProtocol());
BasicAsyncRequestProducer asyncRequestProducer = new BasicAsyncRequestProducer(target, request);
BasicAsyncResponseConsumer asyncResponseConsumer = new BasicAsyncResponseConsumer();
RequestAsyncFuture<T> futureCallback = new RequestAsyncFuture<>(returnType, callback);
BasicHttpContext httpContext = new BasicHttpContext();
requester.execute(asyncRequestProducer, asyncResponseConsumer, pool, httpContext, futureCallback);
return (callback instanceof JsonRpcFuture ? (Future<T>) callback : null);
}
/**
* Set the request headers.
*
* @param request the request object
* @param headers to be used
*/
private void addHeaders(HttpRequest request, Map<String, String> headers) {
for (Map.Entry<String, String> key : headers.entrySet()) {
request.addHeader(key.getKey(), key.getValue());
}
}
/**
* Writes a request.
*
* @param methodName the method name
* @param arguments the arguments
* @param httpRequest the stream on error
*/
private void writeRequest(String methodName, Object arguments, HttpRequest httpRequest) throws IOException {
ObjectNode request = mapper.createObjectNode();
request.put(ID, nextId.getAndIncrement());
request.put(JSONRPC, JsonRpcBasicServer.VERSION);
request.put(METHOD, methodName);
if (arguments != null && arguments.getClass().isArray()) {
Object[] args = Object[].class.cast(arguments);
if (args.length > 0) {
request.set(PARAMS, mapper.valueToTree(Object[].class.cast(arguments)));
}
} else if (arguments != null && Collection.class.isInstance(arguments)) {
if (!Collection.class.cast(arguments).isEmpty()) {
request.set(PARAMS, mapper.valueToTree(arguments));
}
} else if (arguments != null && Map.class.isInstance(arguments)) {
if (!Map.class.cast(arguments).isEmpty()) {
request.set(PARAMS, mapper.valueToTree(arguments));
}
} else if (arguments != null) {
request.set(PARAMS, mapper.valueToTree(arguments));
}
logger.debug("JSON-PRC Request: {}", request);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(512);
mapper.writeValue(byteArrayOutputStream, request);
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) httpRequest;
HttpEntity entity;
if (entityRequest.getFirstHeader("Content-Type") == null) {
entity = new ByteArrayEntity(byteArrayOutputStream.toByteArray(), ContentType.APPLICATION_JSON);
} else {
entity = new ByteArrayEntity(byteArrayOutputStream.toByteArray());
}
entityRequest.setEntity(entity);
}
/**
* Invokes the given method with the given arguments and returns
* immediately. The {@code Future<T>} object that is returned can be used to
* retrieve the result.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @param returnType the return type
* @param <T> the return type
* @return the response {@code Future<T>}
*/
public <T> Future<T> invoke(String methodName, Object argument, Class<T> returnType) {
return invoke(methodName, argument, returnType, new HashMap<String, String>());
}
/**
* Invokes the given method with the given arguments and invokes the
* {@code JsonRpcCallback} with the result.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @param callback the {@code JsonRpcCallback}
*/
public void invoke(String methodName, Object argument, JsonRpcCallback<Object> callback) {
invoke(methodName, argument, Object.class, new HashMap<String, String>(), callback);
}
/**
* Invokes the given method with the given arguments and invokes the
* {@code JsonRpcCallback} with the result cast to the given
* {@code returnType}, or null if void. The {@code extraHeaders} are added
* to the request.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @param returnType the return type
* @param extraHeaders extra headers to add to the request
* @param callback the {@code JsonRpcCallback}
*/
private <T> void invoke(String methodName, Object argument, Class<T> returnType, Map<String, String> extraHeaders, JsonRpcCallback<T> callback) {
doInvoke(methodName, argument, returnType, extraHeaders, callback);
}
/**
* Invokes the given method with the given arguments and invokes the
* {@code JsonRpcCallback} with the result cast to the given
* {@code returnType}, or null if void.
*
* @param methodName the name of the method to invoke
* @param argument the arguments to the method
* @param returnType the return type
* @param <T> the return type
* @param callback the {@code JsonRpcCallback}
*/
public <T> void invoke(String methodName, Object argument, Class<T> returnType, JsonRpcCallback<T> callback) {
invoke(methodName, argument, returnType, new HashMap<String, String>(), callback);
}
/**
* Reads a JSON-PRC response from the server. This blocks until a response
* is received.
*
* @param returnType the expected return type
* @param ips the {@link InputStream} to read from
* @return the object returned by the JSON-RPC response
* @throws Throwable on error
*/
private <T> T readResponse(Type returnType, InputStream ips) throws Throwable {
JsonNode response = mapper.readTree(new NoCloseInputStream(ips));
logger.debug("JSON-PRC Response: {}", response);
if (!response.isObject()) {
throw new JsonRpcClientException(0, "Invalid JSON-RPC response", response);
}
ObjectNode jsonObject = ObjectNode.class.cast(response);
if (jsonObject.has(ERROR) && jsonObject.get(ERROR) != null && !jsonObject.get(ERROR).isNull()) {
throw exceptionResolver.resolveException(jsonObject);
}
if (jsonObject.has(RESULT) && !jsonObject.get(RESULT).isNull() && jsonObject.get(RESULT) != null) {
JsonParser returnJsonParser = mapper.treeAsTokens(jsonObject.get(RESULT));
JavaType returnJavaType = mapper.getTypeFactory().constructType(returnType);
return mapper.readValue(returnJsonParser, returnJavaType);
}
return null;
}
private void initialize() {
if (initialized.getAndSet(true)) {
return;
}
IOReactorConfig.Builder config = createConfig();
// params.setParameter(CoreProtocolPNames.USER_AGENT, "jsonrpc4j/1.0");
final ConnectingIOReactor ioReactor = createIoReactor(config);
createSslContext();
int socketBufferSize = Integer.getInteger("com.googlecode.jsonrpc4j.async.socket.buffer", 8 * 1024);
final ConnectionConfig connectionConfig = ConnectionConfig.custom().setBufferSize(socketBufferSize).build();
BasicNIOConnFactory nioConnFactory = new BasicNIOConnFactory(sslContext, null, connectionConfig);
pool = new BasicNIOConnPool(ioReactor, nioConnFactory, Integer.getInteger("com.googlecode.jsonrpc4j.async.connect.timeout", 30000));
pool.setDefaultMaxPerRoute(Integer.getInteger("com.googlecode.jsonrpc4j.async.max.inflight.route", 500));
pool.setMaxTotal(Integer.getInteger("com.googlecode.jsonrpc4j.async.max.inflight.total", 500));
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
HttpAsyncRequestExecutor protocolHandler = new HttpAsyncRequestExecutor();
IOEventDispatch ioEventDispatch = new DefaultHttpClientIODispatch(protocolHandler, sslContext, connectionConfig);
ioReactor.execute(ioEventDispatch);
} catch (InterruptedIOException ex) {
System.err.println("Interrupted");
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
}
}
}, "jsonrpc4j HTTP IOReactor");
t.setDaemon(true);
t.start();
HttpProcessor httpProcessor = new ImmutableHttpProcessor(new RequestContent(), new RequestTargetHost(), new RequestConnControl(), new RequestUserAgent(), new RequestExpectContinue(false));
requester = new HttpAsyncRequester(httpProcessor, new DefaultConnectionReuseStrategy());
}
private IOReactorConfig.Builder createConfig() {
IOReactorConfig.Builder config = IOReactorConfig.custom();
config = config.setSoTimeout(Integer.getInteger("com.googlecode.jsonrpc4j.async.socket.timeout", 30000));
config = config.setConnectTimeout(Integer.getInteger("com.googlecode.jsonrpc4j.async.connect.timeout", 30000));
config = config.setTcpNoDelay(Boolean.valueOf(System.getProperty("com.googlecode.jsonrpc4j.async.tcp.nodelay", "true")));
config = config.setIoThreadCount(Integer.getInteger("com.googlecode.jsonrpc4j.async.reactor.threads", 1));
return config;
}
private ConnectingIOReactor createIoReactor(IOReactorConfig.Builder config) {
final ConnectingIOReactor ioReactor;
try {
ioReactor = new DefaultConnectingIOReactor(config.build());
} catch (IOReactorException e) {
throw new RuntimeException("Exception initializing asynchronous Apache HTTP Client", e);
}
return ioReactor;
}
private void createSslContext() {
if (sslContext == null) {
try {
sslContext = SSLContext.getDefault();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private static class JsonRpcFuture<T> implements Future<T>, JsonRpcCallback<T> {
private T object;
private boolean done;
private ExecutionException exception;
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
public boolean isCancelled() {
return false;
}
public synchronized boolean isDone() {
return done;
}
public synchronized T get() throws InterruptedException,
ExecutionException {
while (!done) {
this.wait();
}
if (exception != null) {
throw exception;
}
return object;
}
public synchronized T get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException,
TimeoutException {
while (!done) {
this.wait(unit.toMillis(timeout));
}
if (exception != null) {
throw exception;
}
return object;
}
public synchronized void onComplete(T result) {
object = result;
done = true;
this.notify();
}
public synchronized void onError(Throwable t) {
exception = new ExecutionException(t);
done = true;
this.notify();
}
}
/**
* Private class to handleRequest the HttpResponse callback.
*
* @param <T>
*/
private class RequestAsyncFuture<T> implements FutureCallback<HttpResponse> {
private final JsonRpcCallback<T> callBack;
private final Class<T> type;
RequestAsyncFuture(Class<T> type, JsonRpcCallback<T> callBack) {
this.type = type;
this.callBack = callBack;
}
public void completed(final HttpResponse response) {
try {
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();
InputStream stream;
if (statusCode == 200) {
HttpEntity entity = response.getEntity();
try {
stream = entity.getContent();
} catch (Exception e) {
failed(e);
return;
}
callBack.onComplete(type.cast(readResponse(type, stream)));
} else {
callBack.onError(new RuntimeException(
"Unexpected response code: " + statusCode));
}
} catch (Throwable t) {
callBack.onError(t);
}
}
public void failed(final Exception ex) {
callBack.onError(ex);
}
public void cancelled() {
callBack.onError(new RuntimeException("HTTP Request was cancelled"));
}
}
}