Skip to content

Remove java http client javax dependency and fix PATCH request for new JDK versions #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ workflows:
version: "15.0.0"
requires:
- java14
- unit-tests:
name: java16
image: "cimg/openjdk"
version: "16.0.0"
requires:
- java15
- unit-tests:
name: java17
image: "cimg/openjdk"
version: "17.0.0"
requires:
- java16
- unit-tests:
name: java18
image: "cimg/openjdk"
version: "18.0.1"
requires:
- java17
- unit-tests:
name: java19
image: "cimg/openjdk"
version: "19.0.0"
requires:
- java18

jobs:
unit-tests:
Expand Down
23 changes: 3 additions & 20 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,10 @@

<dependencies>

<!-- Jersey Http client -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
</dependency>

<!-- Jersey client dependency - as of version 2.26 HK2 hard dependency is removed.
Therefore Jersey HK2 custom InjectionManagerFactory implementation is required.
Details: https://stackoverflow.com/questions/44088493/jersey-stopped-working-with-injectionmanagerfactory-not-found -->
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
</dependency>

<!-- Jersey client requires this dependency -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5-fluent</artifactId>
<version>5.2.1</version>
</dependency>

<!-- Serialization to JSON -->
Expand Down
141 changes: 61 additions & 80 deletions src/main/java/com/postmarkapp/postmark/client/HttpClient.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.postmarkapp.postmark.client;

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.HttpUrlConnectorProvider;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.Response;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.util.Timeout;

import java.io.IOException;
import java.util.Map;

/**
Expand All @@ -30,21 +31,35 @@ public enum DEFAULTS {
}

private final Map<String,Object> headers;
private final Client client;
private CloseableHttpClient client;

private boolean secureConnection = true;
private int connectTimeoutSeconds = DEFAULTS.CONNECT_TIMEOUT_SECONDS.value;
private int readTimeoutSeconds = DEFAULTS.READ_TIMEOUT_SECONDS.value;

public HttpClient(Map<String,Object> headers, int connectTimeoutSeconds, int readTimeoutSeconds) {
this(headers);
setConnectTimeoutSeconds(connectTimeoutSeconds);
setReadTimeoutSeconds(readTimeoutSeconds);
this.headers = headers;
this.connectTimeoutSeconds = connectTimeoutSeconds;
this.readTimeoutSeconds = readTimeoutSeconds;
buildClientWithCustomConfig();
}

public HttpClient(Map<String,Object> headers) {
this.headers = headers;
this.client = buildClient();
setReadTimeoutSeconds(DEFAULTS.READ_TIMEOUT_SECONDS.value);
setConnectTimeoutSeconds(DEFAULTS.CONNECT_TIMEOUT_SECONDS.value);
buildClientWithCustomConfig();
}

/**
* Overload method for executing requests, which doesn't contain data
*
* @param request_type type of HTTP request
* @param url request URL
* @return simplified HTTP request response (status and response text)
*
* @see #execute(REQUEST_TYPES, String, String) for details
*/
public ClientResponse execute(REQUEST_TYPES request_type, String url) throws IOException {
return execute(request_type, url, null);
}

/**
Expand All @@ -56,57 +71,50 @@ public HttpClient(Map<String,Object> headers) {
* @param data data sent for POST/PUT requests
* @return response from HTTP request
*/
public ClientResponse execute(REQUEST_TYPES requestType, String url, String data) {
Response response;
final Builder requestBuilder = clientRequestBuilder((url));
public ClientResponse execute(REQUEST_TYPES requestType, String url, String data) throws IOException {
ClassicHttpRequest request;

switch (requestType) {
case POST:
response = requestBuilder.post(Entity.json(data), Response.class);
request = ClassicRequestBuilder.post(getHttpUrl(url)).setEntity(data).build();
break;

case PUT:
response = requestBuilder.put(Entity.json(data), Response.class);
request = ClassicRequestBuilder.put(getHttpUrl(url)).setEntity(data).build();
break;

case PATCH:
response = requestBuilder.method("PATCH", Entity.json(data), Response.class);
request = ClassicRequestBuilder.patch(getHttpUrl(url)).setEntity(data).build();
break;

case DELETE:
response = requestBuilder.delete(Response.class);
request = ClassicRequestBuilder.delete(getHttpUrl(url)).build();
break;

default:
response = requestBuilder.get(Response.class);
request = ClassicRequestBuilder.get(getHttpUrl(url)).build();
break;

}

return transformResponse(response);
}
for (Map.Entry<String, Object> header : headers.entrySet()) {
request.setHeader(header.getKey(), header.getValue().toString());
}

/**
* Overload method for executing requests, which doesn't contain data
*
* @param request_type type of HTTP request
* @param url request URL
* @return simplified HTTP request response (status and response text)
*
* @see #execute(REQUEST_TYPES, String, String) for details
*/
public ClientResponse execute(REQUEST_TYPES request_type, String url) {
return execute(request_type, url, null);
return client.execute(
request,
response -> new ClientResponse(response.getCode(), EntityUtils.toString(response.getEntity())));
}

// Setters and Getters

public void setConnectTimeoutSeconds(int connectTimeoutSeconds) {
client.property(ClientProperties.CONNECT_TIMEOUT, connectTimeoutSeconds * 1000);
this.connectTimeoutSeconds = connectTimeoutSeconds;
buildClientWithCustomConfig();
}

public void setReadTimeoutSeconds(int readTimeoutSeconds) {
client.property(ClientProperties.READ_TIMEOUT, readTimeoutSeconds * 1000);
this.readTimeoutSeconds = readTimeoutSeconds;
buildClientWithCustomConfig();
}

public void setSecureConnection(boolean secureConnection) {
Expand All @@ -123,59 +131,32 @@ private String getHttpUrl(String url) {
*
* @return original HTTP client
*/
public Client getClient() {
public CloseableHttpClient getClient() {
return client;
}

/**
* Build default HTTP client used for requests
*
* Jersey client uses HttpUrlConnection by default which doesn't have PATCH method:
* https://docs.oracle.com/javase/8/docs/api/java/net/HttpURLConnection.html#setRequestMethod-java.lang.String-
* https://github.com/eclipse-ee4j/jersey/issues/4825
*
* Workaround for being able to do PATCH requests is by using reflection, which would work up to Java 16.
* https://stackoverflow.com/questions/55778145/how-to-use-patch-method-with-jersey-invocation-builder
*
* @return initialized HTTP client
*/
private Client buildClient() {
Client client = ClientBuilder.newClient();
// this allows calls to PATCH by using reflection
client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
return client;
}

private Builder clientRequestBuilder(String url) {
Builder requestBuilder = client.target(getHttpUrl(url)).request();

for (Map.Entry<String, Object> header : headers.entrySet()) {
requestBuilder.header(header.getKey(), header.getValue());
}

return requestBuilder;
}

/**
* Build HTTP client with custom config used for requests
* like:
*
* ClientConfig clientConfig = new ClientConfig();
* clientConfig.connectorProvider(new GrizzlyConnectorProvider());
* clientConfig.connectorProvider(new HttpUrlConnectorProvider().useSetMethodWorkaround());
*/
private Client buildClient(ClientConfig config) {
return ClientBuilder.newClient(config);
private CloseableHttpClient buildClient() {
return HttpClients.createDefault();
}

/**
* Gets simplified HTTP request response that will contain only response and status.
* Build HTTP client used for requests with custom config settings
*
* @param response HTTP request response result
* @return simplified HTTP request response
* @return initialized HTTP client
*/
private ClientResponse transformResponse(Response response) {
return new ClientResponse(response.getStatus(), response.readEntity(String.class));
private void buildClientWithCustomConfig() {
RequestConfig requestConfig = RequestConfig
.custom()
.setConnectTimeout(Timeout.ofSeconds(connectTimeoutSeconds))
.setResponseTimeout(Timeout.ofSeconds(readTimeoutSeconds))
.build();

this.client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).build();
}

/**
Expand Down