Skip to content

Commit 4235401

Browse files
committed
refactor: use okhttp for all transports
1 parent 4f30d1b commit 4235401

8 files changed

Lines changed: 260 additions & 303 deletions

File tree

pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4242
<jackson.version>2.17.2</jackson.version>
4343
<junit.version>5.11.0</junit.version>
44+
<okhttp.version>4.12.0</okhttp.version>
4445
</properties>
4546

4647
<dependencies>
@@ -56,12 +57,25 @@
5657
<version>${jackson.version}</version>
5758
</dependency>
5859

60+
<dependency>
61+
<groupId>com.squareup.okhttp3</groupId>
62+
<artifactId>okhttp</artifactId>
63+
<version>${okhttp.version}</version>
64+
</dependency>
65+
5966
<dependency>
6067
<groupId>org.junit.jupiter</groupId>
6168
<artifactId>junit-jupiter</artifactId>
6269
<version>${junit.version}</version>
6370
<scope>test</scope>
6471
</dependency>
72+
73+
<dependency>
74+
<groupId>com.squareup.okhttp3</groupId>
75+
<artifactId>mockwebserver</artifactId>
76+
<version>${okhttp.version}</version>
77+
<scope>test</scope>
78+
</dependency>
6579
</dependencies>
6680

6781
<build>

src/main/java/io/facturapi/http/FacturapiConfig.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
88
import io.facturapi.ApiVersion;
99
import io.facturapi.constants.FacturapiConstants;
10-
import java.net.http.HttpClient;
1110
import java.time.Duration;
1211
import java.util.Objects;
12+
import okhttp3.OkHttpClient;
1313

1414
public final class FacturapiConfig {
1515
private final String apiKey;
1616
private final ApiVersion apiVersion;
1717
private final String baseUrl;
1818
private final Duration timeout;
1919
private final String userAgent;
20-
private final HttpClient httpClient;
20+
private final OkHttpClient httpClient;
2121
private final ObjectMapper objectMapper;
2222

2323
private FacturapiConfig(Builder builder) {
@@ -50,7 +50,7 @@ public String getUserAgent() {
5050
return userAgent;
5151
}
5252

53-
public HttpClient getHttpClient() {
53+
public OkHttpClient getHttpClient() {
5454
return httpClient;
5555
}
5656

@@ -68,7 +68,7 @@ public static final class Builder {
6868
private String baseUrl;
6969
private Duration timeout = Duration.ofSeconds(30);
7070
private final String userAgent = "facturapi-java/0.1.x";
71-
private HttpClient httpClient;
71+
private OkHttpClient httpClient;
7272
private ObjectMapper objectMapper;
7373

7474
private Builder(String apiKey) {
@@ -93,7 +93,7 @@ public Builder timeout(Duration timeout) {
9393
return this;
9494
}
9595

96-
public Builder httpClient(HttpClient httpClient) {
96+
public Builder httpClient(OkHttpClient httpClient) {
9797
this.httpClient = Objects.requireNonNull(httpClient, "httpClient is required");
9898
return this;
9999
}
@@ -103,8 +103,11 @@ public FacturapiConfig build() {
103103
? FacturapiConstants.BASE_URL_V1
104104
: FacturapiConstants.BASE_URL_V2;
105105

106-
HttpClient resolvedClient = this.httpClient == null
107-
? HttpClient.newBuilder().connectTimeout(timeout).build()
106+
OkHttpClient resolvedClient = this.httpClient == null
107+
? new OkHttpClient.Builder()
108+
.connectTimeout(timeout)
109+
.callTimeout(timeout)
110+
.build()
108111
: this.httpClient;
109112

110113
ObjectMapper resolvedMapper = new ObjectMapper()

src/main/java/io/facturapi/http/FacturapiHttpClient.java

Lines changed: 110 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,35 @@
44
import com.fasterxml.jackson.databind.JsonNode;
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import io.facturapi.FacturapiException;
7+
import java.io.ByteArrayInputStream;
78
import java.io.IOException;
89
import java.io.InputStream;
9-
import java.net.URI;
10+
import java.io.UncheckedIOException;
1011
import java.net.URLEncoder;
11-
import java.net.http.HttpClient;
12-
import java.net.http.HttpRequest;
13-
import java.net.http.HttpResponse;
1412
import java.nio.charset.StandardCharsets;
15-
import java.time.Duration;
1613
import java.util.ArrayList;
1714
import java.util.List;
1815
import java.util.Map;
1916
import java.util.Objects;
17+
import okhttp3.MediaType;
18+
import okhttp3.OkHttpClient;
19+
import okhttp3.Request;
20+
import okhttp3.RequestBody;
21+
import okhttp3.Response;
22+
import okhttp3.ResponseBody;
2023

2124
public final class FacturapiHttpClient {
22-
private final HttpClient httpClient;
25+
private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");
26+
27+
private final OkHttpClient httpClient;
2328
private final ObjectMapper objectMapper;
24-
private final Duration timeout;
2529
private final String baseUrl;
2630
private final String apiKey;
2731
private final String userAgent;
2832

2933
public FacturapiHttpClient(FacturapiConfig config) {
3034
this.httpClient = config.getHttpClient();
3135
this.objectMapper = config.getObjectMapper();
32-
this.timeout = config.getTimeout();
3336
this.baseUrl = config.getBaseUrl();
3437
this.apiKey = config.getApiKey();
3538
this.userAgent = config.getUserAgent();
@@ -98,62 +101,65 @@ private JsonNode requestJsonNode(
98101
Object body,
99102
MultipartBody multipartBody
100103
) {
101-
try {
102-
HttpRequest request = buildRequest(method, path, queryParams, body, multipartBody);
103-
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
104-
validateResponse(response);
104+
Request request = buildRequest(method, path, queryParams, body, multipartBody);
105+
try (Response response = httpClient.newCall(request).execute()) {
106+
if (!response.isSuccessful()) {
107+
throw buildApiException(readBodyText(response), response.code());
108+
}
105109

106-
byte[] responseBytes = response.body();
107-
if (responseBytes == null || responseBytes.length == 0) {
110+
ResponseBody responseBody = response.body();
111+
if (responseBody == null) {
108112
return objectMapper.nullNode();
109113
}
110114

111-
String contentType = response.headers().firstValue("Content-Type").orElse("");
115+
byte[] responseBytes = responseBody.bytes();
116+
if (responseBytes.length == 0) {
117+
return objectMapper.nullNode();
118+
}
119+
120+
String contentType = responseBody.contentType() == null ? "" : responseBody.contentType().toString();
112121
if (!contentType.contains("application/json")) {
113122
return objectMapper.valueToTree(new String(responseBytes, StandardCharsets.UTF_8));
114123
}
115124
return objectMapper.readTree(responseBytes);
116125
} catch (IOException e) {
117126
throw new FacturapiException("I/O error when calling Facturapi API", e);
118-
} catch (InterruptedException e) {
119-
Thread.currentThread().interrupt();
120-
throw new FacturapiException("Request interrupted", e);
121127
}
122128
}
123129

124130
private byte[] requestBytes(String method, String path, Object body) {
125-
try {
126-
HttpRequest request = buildRequest(method, path, null, body, null);
127-
HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
128-
validateResponse(response);
129-
return response.body();
131+
Request request = buildRequest(method, path, null, body, null);
132+
try (Response response = httpClient.newCall(request).execute()) {
133+
if (!response.isSuccessful()) {
134+
throw buildApiException(readBodyText(response), response.code());
135+
}
136+
ResponseBody responseBody = response.body();
137+
return responseBody == null ? new byte[0] : responseBody.bytes();
130138
} catch (IOException e) {
131139
throw new FacturapiException("I/O error when calling Facturapi API", e);
132-
} catch (InterruptedException e) {
133-
Thread.currentThread().interrupt();
134-
throw new FacturapiException("Request interrupted", e);
135140
}
136141
}
137142

138143
private InputStream requestStream(String method, String path, Object body) {
144+
Request request = buildRequest(method, path, null, body, null);
139145
try {
140-
HttpRequest request = buildRequest(method, path, null, body, null);
141-
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
142-
int statusCode = response.statusCode();
143-
if (statusCode < 200 || statusCode >= 300) {
144-
byte[] responseBytes;
145-
try (InputStream errorStream = response.body()) {
146-
responseBytes = errorStream == null ? new byte[0] : errorStream.readAllBytes();
146+
Response response = httpClient.newCall(request).execute();
147+
if (!response.isSuccessful()) {
148+
try {
149+
throw buildApiException(readBodyText(response), response.code());
150+
} finally {
151+
response.close();
147152
}
148-
String bodyText = responseBytes.length == 0 ? "" : new String(responseBytes, StandardCharsets.UTF_8);
149-
throw buildApiException(bodyText, statusCode);
150153
}
151-
return response.body();
154+
155+
ResponseBody responseBody = response.body();
156+
if (responseBody == null) {
157+
response.close();
158+
return new ByteArrayInputStream(new byte[0]);
159+
}
160+
return new ResponseInputStream(response, responseBody);
152161
} catch (IOException e) {
153162
throw new FacturapiException("I/O error when calling Facturapi API", e);
154-
} catch (InterruptedException e) {
155-
Thread.currentThread().interrupt();
156-
throw new FacturapiException("Request interrupted", e);
157163
}
158164
}
159165

@@ -255,6 +261,9 @@ private FacturapiException buildApiException(String bodyText, int statusCode) {
255261
errorPath = pathNode.asText();
256262
}
257263

264+
if (firstDefined(root, "message", "error", "detail") == null) {
265+
message = bodyText;
266+
}
258267
}
259268
} catch (Exception ignored) {
260269
message = bodyText.isBlank() ? message : bodyText;
@@ -264,41 +273,50 @@ private FacturapiException buildApiException(String bodyText, int statusCode) {
264273
return new FacturapiException(message, resolvedStatus, errorCode, errorPath);
265274
}
266275

267-
private HttpRequest buildRequest(
276+
private static String readBodyText(Response response) throws IOException {
277+
ResponseBody responseBody = response.body();
278+
if (responseBody == null) {
279+
return "";
280+
}
281+
return responseBody.string();
282+
}
283+
284+
private Request buildRequest(
268285
String method,
269286
String path,
270287
Map<String, ?> queryParams,
271288
Object body,
272289
MultipartBody multipartBody
273-
) throws IOException {
274-
URI uri = URI.create(baseUrl + normalizePath(path) + buildQuery(queryParams));
275-
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
276-
.uri(uri)
277-
.timeout(timeout)
290+
) {
291+
String url = baseUrl + normalizePath(path) + buildQuery(queryParams);
292+
Request.Builder builder = new Request.Builder()
293+
.url(url)
278294
.header("Authorization", "Bearer " + apiKey)
279295
.header("Accept", "application/json")
280296
.header("User-Agent", userAgent);
281297

282-
HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.noBody();
298+
RequestBody requestBody = null;
283299
if (multipartBody != null) {
284-
publisher = multipartBody.getPublisher();
285-
requestBuilder.header("Content-Type", multipartBody.getContentType());
300+
requestBody = multipartBody.getBody();
286301
} else if (body != null) {
287-
byte[] json = objectMapper.writeValueAsBytes(body);
288-
publisher = HttpRequest.BodyPublishers.ofByteArray(json);
289-
requestBuilder.header("Content-Type", "application/json");
302+
try {
303+
byte[] json = objectMapper.writeValueAsBytes(body);
304+
requestBody = RequestBody.create(json, JSON_MEDIA_TYPE);
305+
} catch (IOException e) {
306+
throw new UncheckedIOException("Could not serialize request body", e);
307+
}
290308
}
291309

292-
requestBuilder.method(method, publisher);
293-
return requestBuilder.build();
294-
}
295-
296-
private void validateResponse(HttpResponse<byte[]> response) {
297-
int statusCode = response.statusCode();
298-
if (statusCode < 200 || statusCode >= 300) {
299-
String bodyText = response.body() == null ? "" : new String(response.body(), StandardCharsets.UTF_8);
300-
throw buildApiException(bodyText, statusCode);
310+
if (requestBody == null) {
311+
if ("GET".equals(method) || "HEAD".equals(method)) {
312+
builder.method(method, null);
313+
} else {
314+
builder.method(method, RequestBody.create(new byte[0], null));
315+
}
316+
} else {
317+
builder.method(method, requestBody);
301318
}
319+
return builder.build();
302320
}
303321

304322
private static String normalizePath(String path) {
@@ -345,4 +363,36 @@ private static void appendQueryPart(List<String> parts, String key, Object value
345363
String encodedValue = URLEncoder.encode(Objects.toString(value), StandardCharsets.UTF_8);
346364
parts.add(encodedKey + "=" + encodedValue);
347365
}
366+
367+
private static final class ResponseInputStream extends InputStream {
368+
private final Response response;
369+
private final ResponseBody responseBody;
370+
private final InputStream delegate;
371+
372+
private ResponseInputStream(Response response, ResponseBody responseBody) {
373+
this.response = response;
374+
this.responseBody = responseBody;
375+
this.delegate = responseBody.byteStream();
376+
}
377+
378+
@Override
379+
public int read() throws IOException {
380+
return delegate.read();
381+
}
382+
383+
@Override
384+
public int read(byte[] b, int off, int len) throws IOException {
385+
return delegate.read(b, off, len);
386+
}
387+
388+
@Override
389+
public void close() throws IOException {
390+
try {
391+
delegate.close();
392+
} finally {
393+
responseBody.close();
394+
response.close();
395+
}
396+
}
397+
}
348398
}

src/main/java/io/facturapi/http/MultipartBody.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package io.facturapi.http;
22

3-
import java.net.http.HttpRequest;
3+
import okhttp3.RequestBody;
44

55
public final class MultipartBody {
6-
private final HttpRequest.BodyPublisher publisher;
6+
private final RequestBody body;
77
private final String contentType;
88

9-
public MultipartBody(HttpRequest.BodyPublisher publisher, String contentType) {
10-
this.publisher = publisher;
9+
public MultipartBody(RequestBody body, String contentType) {
10+
this.body = body;
1111
this.contentType = contentType;
1212
}
1313

14-
public HttpRequest.BodyPublisher getPublisher() {
15-
return publisher;
14+
public RequestBody getBody() {
15+
return body;
1616
}
1717

1818
public String getContentType() {

0 commit comments

Comments
 (0)