44import com .fasterxml .jackson .databind .JsonNode ;
55import com .fasterxml .jackson .databind .ObjectMapper ;
66import io .facturapi .FacturapiException ;
7+ import java .io .ByteArrayInputStream ;
78import java .io .IOException ;
89import java .io .InputStream ;
9- import java .net . URI ;
10+ import java .io . UncheckedIOException ;
1011import java .net .URLEncoder ;
11- import java .net .http .HttpClient ;
12- import java .net .http .HttpRequest ;
13- import java .net .http .HttpResponse ;
1412import java .nio .charset .StandardCharsets ;
15- import java .time .Duration ;
1613import java .util .ArrayList ;
1714import java .util .List ;
1815import java .util .Map ;
1916import 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
2124public 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}
0 commit comments