From 4f9153b159b28f84e56089f3dcd646fb66141999 Mon Sep 17 00:00:00 2001 From: Andreas Reiter Date: Wed, 19 Jun 2024 08:04:58 +0200 Subject: [PATCH 1/4] added apache http client support --- pom.xml | 6 + src/main/java/si/mazi/rescu/ClientConfig.java | 27 ++- src/main/java/si/mazi/rescu/Config.java | 16 +- src/main/java/si/mazi/rescu/HttpTemplate.java | 110 +++++---- .../si/mazi/rescu/RestInvocationHandler.java | 14 +- .../mazi/rescu/clients/ApacheConnection.java | 210 ++++++++++++++++++ .../si/mazi/rescu/clients/HttpConnection.java | 55 +++++ .../rescu/clients/HttpConnectionType.java | 12 + .../si/mazi/rescu/clients/JavaConnection.java | 126 +++++++++++ .../java/si/mazi/rescu/HttpTemplateTest.java | 22 +- .../mazi/rescu/RestInvocationHandlerTest.java | 57 +++-- .../mazi/rescu/TestRestInvocationHandler.java | 6 +- 12 files changed, 554 insertions(+), 107 deletions(-) create mode 100644 src/main/java/si/mazi/rescu/clients/ApacheConnection.java create mode 100644 src/main/java/si/mazi/rescu/clients/HttpConnection.java create mode 100644 src/main/java/si/mazi/rescu/clients/HttpConnectionType.java create mode 100644 src/main/java/si/mazi/rescu/clients/JavaConnection.java diff --git a/pom.xml b/pom.xml index ac3665c..1fc7fbe 100644 --- a/pom.xml +++ b/pom.xml @@ -139,6 +139,12 @@ signpost-core 2.1.1 + + org.apache.httpcomponents + httpclient + 4.5.14 + provided + diff --git a/src/main/java/si/mazi/rescu/ClientConfig.java b/src/main/java/si/mazi/rescu/ClientConfig.java index f091059..625f3a5 100644 --- a/src/main/java/si/mazi/rescu/ClientConfig.java +++ b/src/main/java/si/mazi/rescu/ClientConfig.java @@ -22,19 +22,22 @@ */ package si.mazi.rescu; +import java.lang.annotation.Annotation; +import java.net.Proxy.Type; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + import com.fasterxml.jackson.databind.ObjectMapper; + import oauth.signpost.OAuthConsumer; +import si.mazi.rescu.clients.HttpConnectionType; import si.mazi.rescu.serialization.jackson.DefaultJacksonObjectMapperFactory; import si.mazi.rescu.serialization.jackson.JacksonConfigureListener; import si.mazi.rescu.serialization.jackson.JacksonObjectMapperFactory; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLSocketFactory; -import java.lang.annotation.Annotation; -import java.net.Proxy.Type; -import java.util.HashMap; -import java.util.Map; - public class ClientConfig { private final Map, Params> defaultParamsMap = new HashMap<>(); @@ -50,6 +53,7 @@ public class ClientConfig { private boolean ignoreHttpErrorCodes; private boolean wrapUnexpectedExceptions; private OAuthConsumer oAuthConsumer; + private HttpConnectionType connectionType; public ClientConfig() { httpConnTimeout = Config.getHttpConnTimeout(); @@ -57,6 +61,7 @@ public ClientConfig() { proxyPort = Config.getProxyPort(); proxyHost = Config.getProxyHost(); proxyType = Config.getProxyType(); + connectionType = Config.getConnectionType(); ignoreHttpErrorCodes = Config.isIgnoreHttpErrorCodes(); wrapUnexpectedExceptions = Config.isWrapUnexpectedExceptions(); } @@ -154,6 +159,14 @@ public Type getProxyType() { public void setProxyType(Type proxyType) { this.proxyType = proxyType; } + + public HttpConnectionType getConnectionType() { + return connectionType; + } + + public void setConnectionType(HttpConnectionType connectionType) { + this.connectionType = connectionType; + } public boolean isIgnoreHttpErrorCodes() { return ignoreHttpErrorCodes; diff --git a/src/main/java/si/mazi/rescu/Config.java b/src/main/java/si/mazi/rescu/Config.java index 7e824d4..1e562b9 100644 --- a/src/main/java/si/mazi/rescu/Config.java +++ b/src/main/java/si/mazi/rescu/Config.java @@ -26,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import si.mazi.rescu.clients.HttpConnectionType; + import java.io.IOException; import java.io.InputStream; import java.net.Proxy; @@ -57,6 +59,8 @@ final class Config { private static final String WRAP_UNEXPECTED_EXCEPTIONS = "rescu.http.wrapUnexpectedExceptions"; + private static final String CONNECTION_TYPE = "rescu.http.connection_type"; + private static final int httpConnTimeout; private static final int httpReadTimeout; @@ -70,6 +74,8 @@ final class Config { private static final boolean ignoreHttpErrorCodes; private static final boolean wrapUnexpectedExceptions; + + private static final HttpConnectionType connectionType; static { Properties dfts = new Properties(); @@ -95,13 +101,15 @@ final class Config { proxyType = Optional.ofNullable(properties.getProperty(PROXY_TYPE)).map(Proxy.Type::valueOf).orElse(null); ignoreHttpErrorCodes = getBoolean(properties, IGNORE_HTTP_ERROR_CODES); wrapUnexpectedExceptions = getBoolean(properties, WRAP_UNEXPECTED_EXCEPTIONS); - + connectionType = Optional.ofNullable(properties.getProperty(CONNECTION_TYPE)).map(HttpConnectionType::valueOf).orElse(HttpConnectionType.DEFAULT); + log.debug("Configuration from rescu.properties:"); log.debug("httpConnTimeout = {}", httpConnTimeout); log.debug("httpReadTimeout = {}", httpReadTimeout); log.debug("proxyHost = {}", proxyHost); log.debug("proxyPort = {}", proxyPort); log.debug("proxyType = {}", proxyType); + log.debug("connectionType = {}", connectionType); log.debug("ignoreHttpErrorCodes = {}", ignoreHttpErrorCodes); } @@ -132,7 +140,11 @@ public static Integer getProxyPort() { public static Type getProxyType() { return proxyType; - } + } + + public static HttpConnectionType getConnectionType() { + return connectionType; + } public static boolean isIgnoreHttpErrorCodes() { return ignoreHttpErrorCodes; diff --git a/src/main/java/si/mazi/rescu/HttpTemplate.java b/src/main/java/si/mazi/rescu/HttpTemplate.java index af0ac8e..cc223d9 100644 --- a/src/main/java/si/mazi/rescu/HttpTemplate.java +++ b/src/main/java/si/mazi/rescu/HttpTemplate.java @@ -22,32 +22,31 @@ */ package si.mazi.rescu; -import oauth.signpost.OAuthConsumer; -import oauth.signpost.exception.OAuthException; -import oauth.signpost.http.HttpRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import si.mazi.rescu.oauth.RescuOAuthRequestAdapter; -import si.mazi.rescu.utils.HttpUtils; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLSocketFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.Proxy; -import java.net.URL; -import java.net.URLConnection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import oauth.signpost.OAuthConsumer; +import si.mazi.rescu.clients.ApacheConnection; +import si.mazi.rescu.clients.HttpConnectionType; +import si.mazi.rescu.clients.JavaConnection; +import si.mazi.rescu.clients.HttpConnection; +import si.mazi.rescu.utils.HttpUtils; + /** * Various HTTP utility methods */ @@ -79,19 +78,22 @@ class HttpTemplate { private final SSLSocketFactory sslSocketFactory; private final HostnameVerifier hostnameVerifier; private final OAuthConsumer oAuthConsumer; + + private final HttpConnectionType connectionType; HttpTemplate(int readTimeout, String proxyHost, Integer proxyPort, Proxy.Type proxyType, - SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, OAuthConsumer oAuthConsumer) { - this(0, readTimeout, proxyHost, proxyPort, proxyType, sslSocketFactory, hostnameVerifier, oAuthConsumer); + SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, OAuthConsumer oAuthConsumer, HttpConnectionType client) { + this(0, readTimeout, proxyHost, proxyPort, proxyType, sslSocketFactory, hostnameVerifier, oAuthConsumer, client); } HttpTemplate(int connTimeout, int readTimeout, String proxyHost, Integer proxyPort, Proxy.Type proxyType, - SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, OAuthConsumer oAuthConsumer) { + SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier, OAuthConsumer oAuthConsumer, HttpConnectionType connectionType) { this.connTimeout = connTimeout; this.readTimeout = readTimeout; this.sslSocketFactory = sslSocketFactory; this.hostnameVerifier = hostnameVerifier; this.oAuthConsumer = oAuthConsumer; + this.connectionType = connectionType; defaultHttpHeaders.put("Accept-Charset", CHARSET_UTF_8); // defaultHttpHeaders.put("Content-Type", "application/x-www-form-urlencoded"); @@ -108,7 +110,7 @@ class HttpTemplate { } } - HttpURLConnection send(String urlString, String requestBody, Map httpHeaders, HttpMethod method) throws IOException { + HttpConnection send(String urlString, String requestBody, Map httpHeaders, HttpMethod method) throws IOException { if (requestBody != null && requestBody.length() > 0) { log.debug("Executing {} request at {} body \n{}", method, urlString, truncate(requestBody, requestMaxLogLen)); } else { @@ -120,9 +122,11 @@ HttpURLConnection send(String urlString, String requestBody, Map preconditionNotNull(httpHeaders, "httpHeaders should not be null"); int contentLength = requestBody == null ? 0 : requestBody.getBytes().length; - HttpURLConnection connection = configureURLConnection(method, urlString, httpHeaders, contentLength); + HttpConnection connection = configureURLConnection(method, urlString, httpHeaders, contentLength); if (oAuthConsumer != null) { + throw new RuntimeException("OAuth not supported yet"); + /* HttpRequest request = new RescuOAuthRequestAdapter(connection, requestBody); try { @@ -130,6 +134,7 @@ HttpURLConnection send(String urlString, String requestBody, Map } catch (OAuthException e) { throw new RuntimeException("OAuth error", e); } + */ } if (contentLength > 0) { @@ -141,7 +146,7 @@ HttpURLConnection send(String urlString, String requestBody, Map return connection; } - InvocationResult receive(HttpURLConnection connection) throws IOException { + InvocationResult receive(HttpConnection connection) throws IOException { int httpStatus = connection.getResponseCode(); log.debug("Request http status = {}", httpStatus); if (log.isTraceEnabled()) { @@ -169,24 +174,24 @@ InvocationResult receive(HttpURLConnection connection) throws IOException { * @param urlString A string representation of a URL * @param httpHeaders The HTTP headers (will override the defaults) * @param contentLength The Content-Length request property - * @return An HttpURLConnection based on the given parameters + * @return An RescuHttpURLConnection based on the given parameters * @throws IOException If something goes wrong */ - private HttpURLConnection configureURLConnection(HttpMethod method, String urlString, Map httpHeaders, int contentLength) throws IOException { + private HttpConnection configureURLConnection(HttpMethod method, String urlString, Map httpHeaders, int contentLength) throws IOException { preconditionNotNull(method, "method cannot be null"); preconditionNotNull(urlString, "urlString cannot be null"); preconditionNotNull(httpHeaders, "httpHeaders cannot be null"); - HttpURLConnection connection = getHttpURLConnection(urlString); - connection.setRequestMethod(method.name()); + HttpConnection connection = getRescuHttpURLConnection(urlString); + connection.setRequestMethod(method); Map headerKeyValues = new HashMap<>(defaultHttpHeaders); headerKeyValues.putAll(httpHeaders); for (Map.Entry entry : headerKeyValues.entrySet()) { - connection.setRequestProperty(entry.getKey(), entry.getValue()); + connection.addHeader(entry.getKey(), entry.getValue()); log.trace("Header request property: key='{}', value='{}'", entry.getKey(), entry.getValue()); } @@ -195,14 +200,22 @@ private HttpURLConnection configureURLConnection(HttpMethod method, String urlSt connection.setDoOutput(true); connection.setDoInput(true); } - connection.setRequestProperty("Content-Length", Integer.toString(contentLength)); + connection.addHeader("Content-Length", Integer.toString(contentLength)); return connection; } - protected HttpURLConnection getHttpURLConnection(String urlString) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(urlString).openConnection(proxy); - + protected HttpConnection getRescuHttpURLConnection(String urlString) throws IOException { + + //RescuHttpURLConnection connection = (RescuHttpURLConnection) new URL(urlString).openConnection(proxy); + + HttpConnection connection = null; + switch (connectionType) { + case java: connection = JavaConnection.create(urlString, proxy); break; + case apache: connection = ApacheConnection.create(urlString, proxy); break; + default: throw new RuntimeException("Not supported connection type " + connectionType); + } + if (readTimeout > 0) { connection.setReadTimeout(readTimeout); } @@ -210,15 +223,12 @@ protected HttpURLConnection getHttpURLConnection(String urlString) throws IOExce connection.setConnectTimeout(connTimeout); } - if (connection instanceof HttpsURLConnection) { - HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; - + if (connection.ssl()) { if (sslSocketFactory != null) { - httpsConnection.setSSLSocketFactory(sslSocketFactory); + connection.setSSLSocketFactory(sslSocketFactory); } - if (hostnameVerifier != null) { - httpsConnection.setHostnameVerifier(hostnameVerifier); + connection.setHostnameVerifier(hostnameVerifier); } } @@ -235,7 +245,7 @@ protected HttpURLConnection getHttpURLConnection(String urlString) throws IOExce * @return A String representation of the input stream * @throws IOException If something goes wrong */ - String readInputStreamAsEncodedString(InputStream inputStream, HttpURLConnection connection) throws IOException { + String readInputStreamAsEncodedString(InputStream inputStream, HttpConnection connection) throws IOException { if (inputStream == null) { return null; } @@ -265,32 +275,10 @@ String readInputStreamAsEncodedString(InputStream inputStream, HttpURLConnection } } - boolean izGzipped(HttpURLConnection connection) { + boolean izGzipped(HttpConnection connection) { return "gzip".equalsIgnoreCase(connection.getHeaderField("Content-Encoding")); } - /** - * Determine the response encoding if specified - * - * @param connection The HTTP connection - * @return The response encoding as a string (taken from "Content-Type") - */ - String getResponseEncoding(URLConnection connection) { - - String charset = null; - - String contentType = connection.getHeaderField("Content-Type"); - if (contentType != null) { - for (String param : contentType.replace(" ", "").split(";")) { - if (param.startsWith("charset=")) { - charset = param.split("=", 2)[1]; - break; - } - } - } - return charset; - } - private static void preconditionNotNull(Object what, String message) { if (what == null) { throw new NullPointerException(message); @@ -312,4 +300,8 @@ private String truncate(String toTruncate, int maxLen) { } return toTruncate.substring(0, maxLen); } + + String getResponseEncoding(HttpConnection connection) { + return connection.getResponseEncoding(); + } } diff --git a/src/main/java/si/mazi/rescu/RestInvocationHandler.java b/src/main/java/si/mazi/rescu/RestInvocationHandler.java index edd84e8..f5e196d 100644 --- a/src/main/java/si/mazi/rescu/RestInvocationHandler.java +++ b/src/main/java/si/mazi/rescu/RestInvocationHandler.java @@ -24,6 +24,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import si.mazi.rescu.clients.HttpConnection; import si.mazi.rescu.serialization.PlainTextResponseReader; import si.mazi.rescu.serialization.jackson.DefaultJacksonObjectMapperFactory; import si.mazi.rescu.serialization.jackson.JacksonObjectMapperFactory; @@ -34,7 +36,6 @@ import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.net.HttpURLConnection; import java.util.HashMap; import java.util.Map; @@ -89,7 +90,8 @@ public class RestInvocationHandler implements InvocationHandler { this.config.getProxyType(), this.config.getSslSocketFactory(), this.config.getHostnameVerifier(), - this.config.getOAuthConsumer()); + this.config.getOAuthConsumer(), + this.config.getConnectionType()); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { @@ -99,7 +101,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl RestMethodMetadata methodMetadata = getMetadata(method); - HttpURLConnection connection = null; + HttpConnection connection = null; RestInvocation invocation = null; Object lock = getValueGenerator(args); if (lock == null) { @@ -122,7 +124,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } } - private boolean makeAware(Object result, HttpURLConnection connection, RestInvocation invocation) { + private boolean makeAware(Object result, HttpConnection connection, RestInvocation invocation) { boolean madeAware = false; if (result instanceof InvocationAware) { try { @@ -143,7 +145,7 @@ private boolean makeAware(Object result, HttpURLConnection connection, RestInvoc return madeAware; } - protected HttpURLConnection invokeHttp(RestInvocation invocation) throws IOException { + protected HttpConnection invokeHttp(RestInvocation invocation) throws IOException { RestMethodMetadata methodMetadata = invocation.getMethodMetadata(); RequestWriter requestWriter = requestWriterResolver.resolveWriter(invocation.getMethodMetadata()); @@ -152,7 +154,7 @@ protected HttpURLConnection invokeHttp(RestInvocation invocation) throws IOExcep return httpTemplate.send(invocation.getInvocationUrl(), requestBody, invocation.getAllHttpHeaders(), methodMetadata.getHttpMethod()); } - protected Object receiveAndMap(RestMethodMetadata methodMetadata, HttpURLConnection connection) throws IOException { + protected Object receiveAndMap(RestMethodMetadata methodMetadata, HttpConnection connection) throws IOException { InvocationResult invocationResult = httpTemplate.receive(connection); return mapInvocationResult(invocationResult, methodMetadata); } diff --git a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java new file mode 100644 index 0000000..71e419c --- /dev/null +++ b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java @@ -0,0 +1,210 @@ +package si.mazi.rescu.clients; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; + +import si.mazi.rescu.HttpMethod; + +public class ApacheConnection implements HttpConnection { + + private final String url; + private final Proxy proxy; + private final HttpClientBuilder builder = HttpClients.custom(); + private final RequestConfig.Builder requestConfig = RequestConfig.custom(); + + private boolean executed = false; + private CloseableHttpResponse res; + private HttpMethod method = HttpMethod.GET; + private ByteArrayOutputStream out; + private final Map headers = new HashMap<>(); + + private CloseableHttpResponse response; + + private ApacheConnection(String url, Proxy proxy) { + super(); + this.url = url; + this.proxy = proxy; + } + + public static HttpConnection create(String url, Proxy proxy) throws MalformedURLException, IOException { + return new ApacheConnection(url, proxy); + } + + private void exec() { + if (executed) { + return; + } + + CloseableHttpClient client = builder + .setDefaultRequestConfig(requestConfig.build()) + .build(); + + HttpRequestBase request = createRequest(method, url); + + if (request instanceof HttpEntityEnclosingRequestBase && out != null) { + HttpEntityEnclosingRequestBase req = (HttpEntityEnclosingRequestBase) request; + HttpEntity entity = req.getEntity(); + if (entity == null) { + entity = new ByteArrayEntity(out.toByteArray()); + req.setEntity(entity); + headers.remove("Content-Length"); // need to drop this header here, otherwise we get "Content-Length header already present" + } + } + headers.forEach((name, value) -> request.addHeader(name, value)); + try { + response = client.execute(request); + + } catch (Throwable e) { + throw new RuntimeException(e); + } + executed = true; + } + + private static HttpRequestBase createRequest(HttpMethod method, String url) { + switch(method) { + case GET: return new HttpGet(url); + case POST: return new HttpPost(url); + case PUT: return new HttpPut(url); + case DELETE: return new HttpDelete(url); + case HEAD: return new HttpHead(url); + case OPTIONS: return new HttpOptions(url); + case PATCH: return new HttpPatch(url); + default: throw new RuntimeException("Not supported http method: " + method); + } + } + + @Override + public String getHeaderField(String name) { + try { + return res.getFirstHeader(name).getValue(); + } catch (Exception e) { + return null; + } + } + + @Override + public void setRequestMethod(HttpMethod method) throws ProtocolException { + this.method = method; + } + + @Override + public String getRequestMethod() { + return method.name(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + if(out == null) { + out = new ByteArrayOutputStream(); + } + return out; + } + + @Override + public InputStream getInputStream() throws IOException { + return response.getEntity().getContent(); + } + @Override + public InputStream getErrorStream() throws IOException { + return getInputStream(); + } + + @Override + public void setDoOutput(boolean dooutput) { + // @TODO ??? + } + + @Override + public void setDoInput(boolean doinput) { + // @TODO ??? + } + + @Override + public int getResponseCode() throws IOException { + exec(); + return response.getStatusLine().getStatusCode(); + } + + @Override + public Map> getHeaderFields() { + + Map> result = new HashMap<>(); + Stream.of(response.getAllHeaders()).forEach(h -> { + List list = result.get(h.getName()); + if (list == null) { + list = new ArrayList<>(); + result.put(h.getName(), list); + } + list.add(h.getValue()); + }); + + return result; + } + + @Override + public void addHeader(String key, String value) { + headers.put(key, value); + } + + @Override + /** + * @param timeout an {@code int} that specifies the timeout value to be used in milliseconds + */ + public void setReadTimeout(int readTimeout) { + // @TODO ??? + } + + @Override + /** + * @param timeout an {@code int} that specifies the connect timeout value in milliseconds + */ + public void setConnectTimeout(int connTimeout) { + requestConfig.setConnectTimeout(connTimeout); + } + + @Override + public boolean ssl() { + return url.startsWith("https"); + } + + @Override + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + // @TODO ??? + } + + @Override + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + builder.setSSLHostnameVerifier(hostnameVerifier); + } + +} diff --git a/src/main/java/si/mazi/rescu/clients/HttpConnection.java b/src/main/java/si/mazi/rescu/clients/HttpConnection.java new file mode 100644 index 0000000..9f0b42c --- /dev/null +++ b/src/main/java/si/mazi/rescu/clients/HttpConnection.java @@ -0,0 +1,55 @@ +package si.mazi.rescu.clients; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ProtocolException; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSocketFactory; + +import si.mazi.rescu.HttpMethod; + +public interface HttpConnection { + + String getHeaderField(String name); + String getRequestMethod(); + OutputStream getOutputStream() throws IOException; + int getResponseCode() throws IOException; + Map> getHeaderFields(); + InputStream getInputStream() throws IOException; + InputStream getErrorStream() throws IOException; + void setRequestMethod(HttpMethod method) throws ProtocolException; + void addHeader(String key, String value); + void setDoOutput(boolean b); + void setDoInput(boolean b); + void setReadTimeout(int readTimeout); + void setConnectTimeout(int connTimeout); + + boolean ssl(); + void setSSLSocketFactory(SSLSocketFactory sslSocketFactory); + void setHostnameVerifier(HostnameVerifier hostnameVerifier); + + /** + * Determine the response encoding if specified + * @return The response encoding as a string (taken from "Content-Type") + */ + default String getResponseEncoding() { + + String charset = null; + + String contentType = getHeaderField("Content-Type"); + if (contentType != null) { + for (String param : contentType.replace(" ", "").split(";")) { + if (param.startsWith("charset=")) { + charset = param.split("=", 2)[1]; + break; + } + } + } + return charset; + } + +} diff --git a/src/main/java/si/mazi/rescu/clients/HttpConnectionType.java b/src/main/java/si/mazi/rescu/clients/HttpConnectionType.java new file mode 100644 index 0000000..d8878e4 --- /dev/null +++ b/src/main/java/si/mazi/rescu/clients/HttpConnectionType.java @@ -0,0 +1,12 @@ +package si.mazi.rescu.clients; + +public enum HttpConnectionType { + + /** java.net.HttpURLConnection */ + java, + /** org.apache.httpcomponents:httpclient */ + apache + ; + + public static final HttpConnectionType DEFAULT = java; +} diff --git a/src/main/java/si/mazi/rescu/clients/JavaConnection.java b/src/main/java/si/mazi/rescu/clients/JavaConnection.java new file mode 100644 index 0000000..29473a5 --- /dev/null +++ b/src/main/java/si/mazi/rescu/clients/JavaConnection.java @@ -0,0 +1,126 @@ +package si.mazi.rescu.clients; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URL; +import java.util.List; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +import si.mazi.rescu.HttpMethod; + +public class JavaConnection implements HttpConnection { + + private final HttpURLConnection connection; + + private JavaConnection(HttpURLConnection connection) { + super(); + this.connection = connection; + } + + public static HttpConnection create(String urlString, Proxy proxy) throws MalformedURLException, IOException { + HttpURLConnection c = (HttpURLConnection) new URL(urlString).openConnection(proxy); + return create(c); + } + public static HttpConnection create(HttpURLConnection c) { + return new JavaConnection(c); + } + + + @Override + public String getHeaderField(String name) { + return connection.getHeaderField(name); + } + + @Override + public String getRequestMethod() { + return connection.getRequestMethod(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return connection.getOutputStream(); + } + + @Override + public int getResponseCode() throws IOException { + return connection.getResponseCode(); + } + + @Override + public Map> getHeaderFields() { + return connection.getHeaderFields(); + } + + @Override + public InputStream getInputStream() throws IOException { + return connection.getInputStream(); + } + + @Override + public InputStream getErrorStream() { + return connection.getErrorStream(); + } + + @Override + public void setRequestMethod(HttpMethod method) throws ProtocolException { + connection.setRequestMethod(method.name()); + } + + @Override + public void addHeader(String key, String value) { + connection.setRequestProperty(key, value); + } + + @Override + public void setDoOutput(boolean dooutput) { + connection.setDoOutput(dooutput); + } + + @Override + public void setDoInput(boolean doinput) { + connection.setDoInput(doinput); + } + + @Override + /** + * @param timeout an {@code int} that specifies the timeout value to be used in milliseconds + */ + public void setReadTimeout(int readTimeout) { + connection.setReadTimeout(readTimeout); + } + + @Override + /** + * @param timeout an {@code int} that specifies the connect timeout value in milliseconds + */ + public void setConnectTimeout(int connTimeout) { + connection.setConnectTimeout(connTimeout); + } + + @Override + public boolean ssl() { + return connection instanceof HttpsURLConnection; + } + + @Override + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; + httpsConnection.setSSLSocketFactory(sslSocketFactory); + } + + @Override + public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { + HttpsURLConnection httpsConnection = (HttpsURLConnection) connection; + httpsConnection.setHostnameVerifier(hostnameVerifier); + } + +} diff --git a/src/test/java/si/mazi/rescu/HttpTemplateTest.java b/src/test/java/si/mazi/rescu/HttpTemplateTest.java index 4470294..289c408 100644 --- a/src/test/java/si/mazi/rescu/HttpTemplateTest.java +++ b/src/test/java/si/mazi/rescu/HttpTemplateTest.java @@ -23,6 +23,10 @@ import org.testng.annotations.Test; +import si.mazi.rescu.clients.HttpConnectionType; +import si.mazi.rescu.clients.JavaConnection; +import si.mazi.rescu.clients.HttpConnection; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -45,7 +49,7 @@ public class HttpTemplateTest { @Test public void testGet() throws Exception { final HttpURLConnection mockHttpURLConnection = new MockHttpURLConnection("/example-httpdata.txt"); - HttpTemplate testObject = new MockHttpTemplate(mockHttpURLConnection); + HttpTemplate testObject = new MockHttpTemplate(JavaConnection.create(mockHttpURLConnection)); InvocationResult executeResult = executeRequest(testObject, "http://example.com/ticker", null, new HashMap(), HttpMethod.GET); assertEquals(200, executeResult.getStatusCode()); assertEquals("Test data", executeResult.getHttpBody()); @@ -53,9 +57,9 @@ public void testGet() throws Exception { @Test public void testReadInputStreamAsEncodedString() throws Exception { - HttpTemplate testObject = new HttpTemplate(30000, null, null, null, null, null, null) { - @Override String getResponseEncoding(URLConnection connection) { return "UTF-8"; } - @Override boolean izGzipped(HttpURLConnection connection) { return false; } + HttpTemplate testObject = new HttpTemplate(30000, null, null, null, null, null, null, HttpConnectionType.DEFAULT) { + @Override String getResponseEncoding(HttpConnection connection) { return "UTF-8"; } + @Override boolean izGzipped(HttpConnection connection) { return false; } }; InputStream inputStream = HttpTemplateTest.class.getResourceAsStream("/example-httpdata.txt"); assertEquals("Test data", testObject.readInputStreamAsEncodedString(inputStream, null)); @@ -64,7 +68,7 @@ public void testReadInputStreamAsEncodedString() throws Exception { @Test public void testPostWithError() throws Exception { final HttpURLConnection mockHttpURLConnection = new MockErrorHttpURLConnection("/error.json"); - HttpTemplate testObject = new MockHttpTemplate(mockHttpURLConnection); + HttpTemplate testObject = new MockHttpTemplate(JavaConnection.create(mockHttpURLConnection)); InvocationResult executeResult = executeRequest(testObject, "http://example.org/accountinfo", "Example", new HashMap(), HttpMethod.POST); assertEquals(500, executeResult.getStatusCode()); assertEquals("{\"result\":\"error\",\"error\":\"Order not found\",\"token\":\"unknown_error\"}", executeResult.getHttpBody()); @@ -88,15 +92,15 @@ public static InvocationResult executeRequest(HttpTemplate httpTemplate, String private static class MockHttpTemplate extends HttpTemplate { - private final HttpURLConnection mockHttpURLConnection; + private final HttpConnection mockHttpURLConnection; - public MockHttpTemplate(HttpURLConnection mockHttpURLConnection) { - super(30000, null, null, null, null, null, null); + public MockHttpTemplate(HttpConnection mockHttpURLConnection) { + super(30000, null, null, null, null, null, null, HttpConnectionType.DEFAULT); this.mockHttpURLConnection = mockHttpURLConnection; } @Override - public HttpURLConnection getHttpURLConnection(String urlString) throws IOException { + public HttpConnection getRescuHttpURLConnection(String urlString) throws IOException { return mockHttpURLConnection; } } diff --git a/src/test/java/si/mazi/rescu/RestInvocationHandlerTest.java b/src/test/java/si/mazi/rescu/RestInvocationHandlerTest.java index ab7a1bd..20da1db 100644 --- a/src/test/java/si/mazi/rescu/RestInvocationHandlerTest.java +++ b/src/test/java/si/mazi/rescu/RestInvocationHandlerTest.java @@ -21,7 +21,27 @@ */ package si.mazi.rescu; -import com.google.common.collect.ImmutableMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,27 +49,20 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.testng.internal.collections.Pair; -import si.mazi.rescu.dto.DummyAccountInfo; -import si.mazi.rescu.dto.DummyTicker; -import si.mazi.rescu.dto.GenericResult; -import si.mazi.rescu.dto.Order; + +import com.google.common.collect.ImmutableMap; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.math.BigDecimal; -import java.net.HttpURLConnection; -import java.util.*; -import java.util.concurrent.*; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; +import si.mazi.rescu.clients.JavaConnection; +import si.mazi.rescu.clients.HttpConnection; +import si.mazi.rescu.dto.DummyAccountInfo; +import si.mazi.rescu.dto.DummyTicker; +import si.mazi.rescu.dto.GenericResult; +import si.mazi.rescu.dto.Order; /** * @author Matija Mazi @@ -384,11 +397,11 @@ public void responseHeadersAwareException() throws Exception { mockHeaders.put("X-my-header", Collections.singletonList("My value")); TestRestInvocationHandler testHandler = new TestRestInvocationHandler(ExampleService.class, new ClientConfig(), "{}", 500) { @Override - protected HttpURLConnection invokeHttp(RestInvocation invocation) { + protected HttpConnection invokeHttp(RestInvocation invocation) { super.invokeHttp(invocation); HttpURLConnection mockConnection = Mockito.mock(HttpURLConnection.class); Mockito.when(mockConnection.getHeaderFields()).thenReturn(mockHeaders); - return mockConnection; + return JavaConnection.create(mockConnection); } }; ExampleService proxy = RestProxyFactory.createProxy(ExampleService.class, testHandler); @@ -406,11 +419,11 @@ public void responseHeadersAwareException500() throws Exception { mockHeaders.put("X-my-header", Collections.singletonList("My value")); TestRestInvocationHandler testHandler = new TestRestInvocationHandler(ExampleService.class, new ClientConfig(), "{}", 500) { @Override - protected HttpURLConnection invokeHttp(RestInvocation invocation) { + protected HttpConnection invokeHttp(RestInvocation invocation) { super.invokeHttp(invocation); HttpURLConnection mockConnection = Mockito.mock(HttpURLConnection.class); Mockito.when(mockConnection.getHeaderFields()).thenReturn(mockHeaders); - return mockConnection; + return JavaConnection.create(mockConnection); } }; ExampleService proxy = RestProxyFactory.createProxy(ExampleService.class, testHandler); @@ -428,11 +441,11 @@ public void responseHeadersAwareResult() throws Exception { mockHeaders.put("X-my-header-1", Collections.singletonList("My value for result")); TestRestInvocationHandler testHandler = new TestRestInvocationHandler(ExampleService.class, new ClientConfig(), "{}", 200) { @Override - protected HttpURLConnection invokeHttp(RestInvocation invocation) { + protected HttpConnection invokeHttp(RestInvocation invocation) { super.invokeHttp(invocation); HttpURLConnection mockConnection = Mockito.mock(HttpURLConnection.class); Mockito.when(mockConnection.getHeaderFields()).thenReturn(mockHeaders); - return mockConnection; + return JavaConnection.create(mockConnection); } }; ExampleService proxy = RestProxyFactory.createProxy(ExampleService.class, testHandler); diff --git a/src/test/java/si/mazi/rescu/TestRestInvocationHandler.java b/src/test/java/si/mazi/rescu/TestRestInvocationHandler.java index 2dd6485..fa8dcf0 100644 --- a/src/test/java/si/mazi/rescu/TestRestInvocationHandler.java +++ b/src/test/java/si/mazi/rescu/TestRestInvocationHandler.java @@ -26,6 +26,8 @@ import java.io.IOException; import java.net.HttpURLConnection; +import si.mazi.rescu.clients.HttpConnection; + /** * NOT thread-safe */ @@ -49,13 +51,13 @@ public TestRestInvocationHandler(Class restInterface, ClientConfig config, } @Override - protected HttpURLConnection invokeHttp(RestInvocation invocation) { + protected HttpConnection invokeHttp(RestInvocation invocation) { this.invocation = invocation; return null; } @Override - protected Object receiveAndMap(RestMethodMetadata methodMetadata, HttpURLConnection connection) throws IOException { + protected Object receiveAndMap(RestMethodMetadata methodMetadata, HttpConnection connection) throws IOException { InvocationResult invocationResult = new InvocationResult(getResponseBody(), getResponseStatusCode()); return mapInvocationResult(invocationResult, methodMetadata); } From 453192d6d85b76ff5f6d3bc405e3b73eadfdc95a Mon Sep 17 00:00:00 2001 From: Andreas Reiter Date: Wed, 19 Jun 2024 10:13:00 +0200 Subject: [PATCH 2/4] enabled OAuth for HttpURLConnection (java) again still not supported by apache client though --- src/main/java/si/mazi/rescu/HttpTemplate.java | 16 ++++++++++------ .../si/mazi/rescu/clients/ApacheConnection.java | 5 +++++ .../si/mazi/rescu/clients/HttpConnection.java | 1 + .../si/mazi/rescu/clients/JavaConnection.java | 7 +++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/si/mazi/rescu/HttpTemplate.java b/src/main/java/si/mazi/rescu/HttpTemplate.java index cc223d9..ca7c180 100644 --- a/src/main/java/si/mazi/rescu/HttpTemplate.java +++ b/src/main/java/si/mazi/rescu/HttpTemplate.java @@ -41,10 +41,13 @@ import org.slf4j.LoggerFactory; import oauth.signpost.OAuthConsumer; +import oauth.signpost.exception.OAuthException; +import oauth.signpost.http.HttpRequest; import si.mazi.rescu.clients.ApacheConnection; +import si.mazi.rescu.clients.HttpConnection; import si.mazi.rescu.clients.HttpConnectionType; import si.mazi.rescu.clients.JavaConnection; -import si.mazi.rescu.clients.HttpConnection; +import si.mazi.rescu.oauth.RescuOAuthRequestAdapter; import si.mazi.rescu.utils.HttpUtils; /** @@ -125,16 +128,17 @@ HttpConnection send(String urlString, String requestBody, Map ht HttpConnection connection = configureURLConnection(method, urlString, httpHeaders, contentLength); if (oAuthConsumer != null) { - throw new RuntimeException("OAuth not supported yet"); - /* - HttpRequest request = new RescuOAuthRequestAdapter(connection, requestBody); - + if (connection.getHttpConnectionType() != HttpConnectionType.java) { + throw new RuntimeException("OAuth not supported yet for " + connection.getHttpConnectionType()); + } + JavaConnection jc = (JavaConnection) connection; + + HttpRequest request = new RescuOAuthRequestAdapter(jc.getHttpURLConnection(), requestBody); try { oAuthConsumer.sign(request); } catch (OAuthException e) { throw new RuntimeException("OAuth error", e); } - */ } if (contentLength > 0) { diff --git a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java index 71e419c..4473ba1 100644 --- a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java +++ b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java @@ -207,4 +207,9 @@ public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { builder.setSSLHostnameVerifier(hostnameVerifier); } + @Override + public HttpConnectionType getHttpConnectionType() { + return HttpConnectionType.apache; + } + } diff --git a/src/main/java/si/mazi/rescu/clients/HttpConnection.java b/src/main/java/si/mazi/rescu/clients/HttpConnection.java index 9f0b42c..25f8c7a 100644 --- a/src/main/java/si/mazi/rescu/clients/HttpConnection.java +++ b/src/main/java/si/mazi/rescu/clients/HttpConnection.java @@ -14,6 +14,7 @@ public interface HttpConnection { + HttpConnectionType getHttpConnectionType(); String getHeaderField(String name); String getRequestMethod(); OutputStream getOutputStream() throws IOException; diff --git a/src/main/java/si/mazi/rescu/clients/JavaConnection.java b/src/main/java/si/mazi/rescu/clients/JavaConnection.java index 29473a5..871e422 100644 --- a/src/main/java/si/mazi/rescu/clients/JavaConnection.java +++ b/src/main/java/si/mazi/rescu/clients/JavaConnection.java @@ -34,6 +34,9 @@ public static HttpConnection create(HttpURLConnection c) { return new JavaConnection(c); } + public HttpURLConnection getHttpURLConnection() { + return connection; + } @Override public String getHeaderField(String name) { @@ -123,4 +126,8 @@ public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { httpsConnection.setHostnameVerifier(hostnameVerifier); } + @Override + public HttpConnectionType getHttpConnectionType() { + return HttpConnectionType.java; + } } From 2a89747663a6343b044f187d1283c7769e288330 Mon Sep 17 00:00:00 2001 From: Andreas Reiter Date: Fri, 21 Jun 2024 11:04:25 +0200 Subject: [PATCH 3/4] added proxy support --- .../mazi/rescu/clients/ApacheConnection.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java index 4473ba1..a8ac6e3 100644 --- a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java +++ b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.Proxy; @@ -17,6 +18,7 @@ import javax.net.ssl.SSLSocketFactory; import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -32,6 +34,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; import si.mazi.rescu.HttpMethod; @@ -64,9 +67,16 @@ private void exec() { if (executed) { return; } + HttpClientBuilder clientBuilder = builder + .setDefaultRequestConfig(requestConfig.build()); + if (proxy != null && proxy != Proxy.NO_PROXY) { + InetSocketAddress address = (InetSocketAddress) proxy.address(); + HttpHost proxy = new HttpHost(address.getHostName(), address.getPort()); + DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); + clientBuilder.setRoutePlanner(routePlanner); + } - CloseableHttpClient client = builder - .setDefaultRequestConfig(requestConfig.build()) + CloseableHttpClient client = clientBuilder .build(); HttpRequestBase request = createRequest(method, url); @@ -77,7 +87,6 @@ private void exec() { if (entity == null) { entity = new ByteArrayEntity(out.toByteArray()); req.setEntity(entity); - headers.remove("Content-Length"); // need to drop this header here, otherwise we get "Content-Length header already present" } } headers.forEach((name, value) -> request.addHeader(name, value)); @@ -173,6 +182,9 @@ public Map> getHeaderFields() { @Override public void addHeader(String key, String value) { + if ("Content-Length".equals(key)) { // need to drop this header here, otherwise we get "Content-Length header already present" + return; + } headers.put(key, value); } From 3dc152c50d4dc6e2e781d809f8441abb4c6cd91d Mon Sep 17 00:00:00 2001 From: Andreas Reiter Date: Fri, 21 Jun 2024 11:21:07 +0200 Subject: [PATCH 4/4] proxy --- src/main/java/si/mazi/rescu/clients/ApacheConnection.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java index a8ac6e3..5ea47fb 100644 --- a/src/main/java/si/mazi/rescu/clients/ApacheConnection.java +++ b/src/main/java/si/mazi/rescu/clients/ApacheConnection.java @@ -72,8 +72,7 @@ private void exec() { if (proxy != null && proxy != Proxy.NO_PROXY) { InetSocketAddress address = (InetSocketAddress) proxy.address(); HttpHost proxy = new HttpHost(address.getHostName(), address.getPort()); - DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy); - clientBuilder.setRoutePlanner(routePlanner); + clientBuilder.setProxy(proxy); } CloseableHttpClient client = clientBuilder