package com.googlecode.jsonrpc4j; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Type; import java.net.HttpURLConnection; import java.net.Proxy; import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.ACCEPT_ENCODING; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.CONTENT_ENCODING; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.JSONRPC_CONTENT_TYPE; /** * A JSON-RPC client that uses the HTTP protocol. */ @SuppressWarnings("unused") public class JsonRpcHttpClient extends JsonRpcClient implements IJsonRpcClient { private static final String GZIP = "gzip"; private final Map<String, String> headers = new HashMap<>(); private URL serviceUrl; private Proxy connectionProxy = Proxy.NO_PROXY; private int connectionTimeoutMillis = 60 * 1000; private int readTimeoutMillis = 60 * 1000 * 2; private SSLContext sslContext = null; private HostnameVerifier hostNameVerifier = null; private String contentType = JSONRPC_CONTENT_TYPE; private boolean gzipRequests = false; /** * Creates the {@link JsonRpcHttpClient} 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 JsonRpcHttpClient(URL serviceUrl, Map<String, String> headers) { this(new ObjectMapper(), serviceUrl, headers); } /** * Creates the {@link JsonRpcHttpClient} 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 JsonRpcHttpClient(ObjectMapper mapper, URL serviceUrl, Map<String, String> headers) { this(mapper, serviceUrl, headers, false, false); } /** * Creates the {@link JsonRpcHttpClient} 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 * @param gzipRequests whether gzip the request * @param acceptGzipResponses whether accept gzip response */ public JsonRpcHttpClient(ObjectMapper mapper, URL serviceUrl, Map<String, String> headers, boolean gzipRequests, boolean acceptGzipResponses) { super(mapper); this.serviceUrl = serviceUrl; this.headers.putAll(headers); this.gzipRequests = gzipRequests; if (acceptGzipResponses) { this.headers.put(ACCEPT_ENCODING, GZIP); } } /** * Creates the {@link JsonRpcHttpClient} 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 */ public JsonRpcHttpClient(URL serviceUrl) { this(new ObjectMapper(), serviceUrl, new HashMap<String, String>()); } /** * {@inheritDoc} */ @Override public void invoke(String methodName, Object argument) throws Throwable { invoke(methodName, argument, null, new HashMap<String, String>()); } /** * {@inheritDoc} */ @Override public Object invoke(String methodName, Object argument, Type returnType) throws Throwable { return invoke(methodName, argument, returnType, new HashMap<String, String>()); } /** * {@inheritDoc} */ @Override public Object invoke(String methodName, Object argument, Type returnType, Map<String, String> extraHeaders) throws Throwable { HttpURLConnection connection = prepareConnection(extraHeaders); try { if (this.gzipRequests) { connection.setRequestProperty(CONTENT_ENCODING, GZIP); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (GZIPOutputStream gos = new GZIPOutputStream(baos)) { super.invoke(methodName, argument, gos); } connection.setFixedLengthStreamingMode(baos.size()); connection.connect(); connection.getOutputStream().write(baos.toByteArray()); } else { connection.connect(); try (OutputStream send = connection.getOutputStream()) { super.invoke(methodName, argument, send); } } final boolean useGzip = useGzip(connection); // read and return value try { try (InputStream answer = getStream(connection.getInputStream(), useGzip)) { return super.readResponse(returnType, answer); } } catch (JsonMappingException e) { // JsonMappingException inherits from IOException throw e; } catch (IOException e) { try (InputStream answer = getStream(connection.getErrorStream(), useGzip)) { return super.readResponse(returnType, answer); } catch (IOException ef) { throw new HttpException(readErrorString(connection), ef); } } } finally { connection.disconnect(); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public <T> T invoke(String methodName, Object argument, Class<T> clazz) throws Throwable { return (T) invoke(methodName, argument, Type.class.cast(clazz)); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public <T> T invoke(String methodName, Object argument, Class<T> clazz, Map<String, String> extraHeaders) throws Throwable { return (T) invoke(methodName, argument, Type.class.cast(clazz), extraHeaders); } /** * Prepares a connection to the server. * * @param extraHeaders extra headers to add to the request * @return the unopened connection * @throws IOException */ private HttpURLConnection prepareConnection(Map<String, String> extraHeaders) throws IOException { // create URLConnection HttpURLConnection connection = (HttpURLConnection) serviceUrl.openConnection(connectionProxy); connection.setConnectTimeout(connectionTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); connection.setAllowUserInteraction(false); connection.setDefaultUseCaches(false); connection.setDoInput(true); connection.setDoOutput(true); connection.setUseCaches(false); connection.setInstanceFollowRedirects(true); connection.setRequestMethod("POST"); setupSsl(connection); addHeaders(extraHeaders, connection); return connection; } private boolean useGzip(final HttpURLConnection connection) { String contentEncoding = connection.getHeaderField(CONTENT_ENCODING); return contentEncoding != null && contentEncoding.equalsIgnoreCase(GZIP); } private InputStream getStream(final InputStream inputStream, final boolean useGzip) throws IOException { return useGzip ? new GZIPInputStream(inputStream) : inputStream; } private static String readErrorString(final HttpURLConnection connection) { try (InputStream stream = connection.getErrorStream()) { StringBuilder buffer = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { for (int ch = reader.read(); ch >= 0; ch = reader.read()) { buffer.append((char) ch); } } return buffer.toString(); } catch (IOException e) { return e.getMessage(); } } private void setupSsl(HttpURLConnection connection) { if (HttpsURLConnection.class.isInstance(connection)) { HttpsURLConnection https = HttpsURLConnection.class.cast(connection); if (hostNameVerifier != null) { https.setHostnameVerifier(hostNameVerifier); } if (sslContext != null) { https.setSSLSocketFactory(sslContext.getSocketFactory()); } } } private void addHeaders(Map<String, String> extraHeaders, HttpURLConnection connection) { connection.setRequestProperty("Content-Type", contentType); for (Entry<String, String> entry : headers.entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); } for (Entry<String, String> entry : extraHeaders.entrySet()) { connection.setRequestProperty(entry.getKey(), entry.getValue()); } } /** * @return the serviceUrl */ public URL getServiceUrl() { return serviceUrl; } /** * @param serviceUrl the serviceUrl to set */ public void setServiceUrl(URL serviceUrl) { this.serviceUrl = serviceUrl; } /** * @return the connectionProxy */ public Proxy getConnectionProxy() { return connectionProxy; } /** * @param connectionProxy the connectionProxy to set */ public void setConnectionProxy(Proxy connectionProxy) { this.connectionProxy = connectionProxy; } /** * @return the connectionTimeoutMillis */ public int getConnectionTimeoutMillis() { return connectionTimeoutMillis; } /** * @param connectionTimeoutMillis the connectionTimeoutMillis to set */ public void setConnectionTimeoutMillis(int connectionTimeoutMillis) { this.connectionTimeoutMillis = connectionTimeoutMillis; } /** * @return the readTimeoutMillis */ public int getReadTimeoutMillis() { return readTimeoutMillis; } /** * @param readTimeoutMillis the readTimeoutMillis to set */ public void setReadTimeoutMillis(int readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; } /** * @return the headers */ public Map<String, String> getHeaders() { return Collections.unmodifiableMap(headers); } /** * @param headers the headers to set */ public void setHeaders(Map<String, String> headers) { this.headers.clear(); this.headers.putAll(headers); } /** * @param sslContext the sslContext to set */ public void setSslContext(SSLContext sslContext) { this.sslContext = sslContext; } /** * @param hostNameVerifier the hostNameVerifier to set */ public void setHostNameVerifier(HostnameVerifier hostNameVerifier) { this.hostNameVerifier = hostNameVerifier; } /** * @param contentType the contentType to set */ public void setContentType(String contentType) { this.contentType = contentType; } }