001
002package com.commercetools.http.okhttp4;
003
004import java.io.IOException;
005import java.util.*;
006import java.util.concurrent.CompletableFuture;
007import java.util.concurrent.ExecutorService;
008import java.util.concurrent.TimeUnit;
009import java.util.function.Supplier;
010import java.util.stream.Collectors;
011
012import javax.validation.constraints.NotNull;
013
014import io.vrap.rmf.base.client.*;
015import io.vrap.rmf.base.client.utils.Utils;
016
017import okhttp3.OkHttpClient;
018import okio.GzipSource;
019import okio.Okio;
020
021public class CtOkHttp4Client extends HttpClientBase {
022
023    public static final int MAX_REQUESTS = 64;
024    private final Supplier<OkHttpClient.Builder> clientBuilder = () -> new OkHttpClient.Builder()
025            .connectTimeout(120, TimeUnit.SECONDS)
026            .writeTimeout(120, TimeUnit.SECONDS)
027            .readTimeout(120, TimeUnit.SECONDS)
028            .protocols(Collections.singletonList(okhttp3.Protocol.HTTP_1_1))
029            .addInterceptor(new UnzippingInterceptor());
030
031    private final OkHttpClient okHttpClient;
032
033    public CtOkHttp4Client() {
034        super();
035        okHttpClient = clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)).build();
036    }
037
038    public CtOkHttp4Client(final BuilderOptions options) {
039        super();
040        okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)))
041                .build();
042    }
043
044    public CtOkHttp4Client(final Supplier<OkHttpClient.Builder> builderSupplier) {
045        super();
046        okHttpClient = builderSupplier.get().build();
047    }
048
049    public CtOkHttp4Client(final int maxRequests, final int maxRequestsPerHost) {
050        super();
051        okHttpClient = clientBuilder.get().dispatcher(createDispatcher(maxRequests, maxRequestsPerHost)).build();
052    }
053
054    public CtOkHttp4Client(final int maxRequests, final int maxRequestsPerHost, final BuilderOptions options) {
055        super();
056        okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(maxRequests, maxRequestsPerHost)))
057                .build();
058    }
059
060    public CtOkHttp4Client(final ExecutorService executor) {
061        super(executor);
062        okHttpClient = clientBuilder.get().dispatcher(createDispatcher(executor, MAX_REQUESTS, MAX_REQUESTS)).build();
063    }
064
065    public CtOkHttp4Client(final ExecutorService executor, final BuilderOptions options) {
066        super(executor);
067        okHttpClient = options.plus(clientBuilder.get().dispatcher(createDispatcher(MAX_REQUESTS, MAX_REQUESTS)))
068                .build();
069    }
070
071    public CtOkHttp4Client(final ExecutorService executor, final int maxRequests, final int maxRequestsPerHost) {
072        super(executor);
073        okHttpClient = clientBuilder.get()
074                .dispatcher(createDispatcher(executor, maxRequests, maxRequestsPerHost))
075                .build();
076    }
077
078    public CtOkHttp4Client(final ExecutorService executor, final int maxRequests, final int maxRequestsPerHost,
079            final BuilderOptions options) {
080        super(executor);
081        okHttpClient = options
082                .plus(clientBuilder.get().dispatcher(createDispatcher(executor, maxRequests, maxRequestsPerHost)))
083                .build();
084    }
085
086    public okhttp3.Dispatcher createDispatcher(final int maxRequests, final int maxRequestsPerHost) {
087        final okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher();
088        dispatcher.setMaxRequests(maxRequests);
089        dispatcher.setMaxRequestsPerHost(maxRequestsPerHost);
090        return dispatcher;
091    }
092
093    public okhttp3.Dispatcher createDispatcher(final ExecutorService executor, final int maxRequests,
094            final int maxRequestsPerHost) {
095        final okhttp3.Dispatcher dispatcher = new okhttp3.Dispatcher(executor);
096        dispatcher.setMaxRequests(maxRequests);
097        dispatcher.setMaxRequestsPerHost(maxRequestsPerHost);
098        return dispatcher;
099    }
100
101    private static final String CONTENT_TYPE = "Content-Type";
102    private static final okhttp3.MediaType JSON = okhttp3.MediaType.get("application/json; charset=utf-8");
103    private static final byte[] emptyBody = new byte[0];
104
105    @Override
106    public CompletableFuture<ApiHttpResponse<byte[]>> execute(final ApiHttpRequest request) {
107        return makeRequest(okHttpClient, toRequest(request)).thenApplyAsync(CtOkHttp4Client::toResponse, executor());
108
109    }
110
111    private static ApiHttpResponse<byte[]> toResponse(final okhttp3.Response response) {
112        final ApiHttpHeaders apiHttpHeaders = new ApiHttpHeaders(response.headers()
113                .toMultimap()
114                .entrySet()
115                .stream()
116                .flatMap(e -> e.getValue().stream().map(value -> ApiHttpHeaders.headerEntry(e.getKey(), value)))
117                .collect(Collectors.toList()));
118
119        final ApiHttpResponse<byte[]> apiHttpResponse = new ApiHttpResponse<>(response.code(), apiHttpHeaders,
120            Optional.ofNullable(response.body())
121                    .map(Utils.wrapToCompletionException(okhttp3.ResponseBody::bytes))
122                    .orElse(null),
123            response.message());
124        if (apiHttpResponse.getBody() != null) {
125            response.close();
126        }
127        return apiHttpResponse;
128    }
129
130    private static okhttp3.Request toRequest(final ApiHttpRequest apiHttpRequest) {
131
132        okhttp3.Request.Builder httpRequestBuilder = new okhttp3.Request.Builder().url(apiHttpRequest.getUrl());
133
134        //set headers
135        for (Map.Entry<String, String> entry : apiHttpRequest.getHeaders().getHeaders()) {
136            httpRequestBuilder = httpRequestBuilder.header(entry.getKey(), entry.getValue());
137        }
138
139        if (apiHttpRequest.getMethod() == null) {
140            throw new IllegalStateException("apiHttpRequest method should be non null");
141        }
142
143        //default media type is JSON, if other media type is set as a header, use it
144        okhttp3.MediaType mediaType = JSON;
145        if (apiHttpRequest.getHeaders()
146                .getHeaders()
147                .stream()
148                .anyMatch(s -> s.getKey().equalsIgnoreCase(CONTENT_TYPE))) {
149            mediaType = okhttp3.MediaType
150                    .get(Objects.requireNonNull(apiHttpRequest.getHeaders().getFirst(ApiHttpHeaders.CONTENT_TYPE)));
151        }
152
153        try {
154            final okhttp3.RequestBody body = apiHttpRequest.getBody() == null ? null
155                    : okhttp3.RequestBody.create(apiHttpRequest.getBody(), mediaType);
156            httpRequestBuilder.method(apiHttpRequest.getMethod().name(), body);
157        }
158        catch (NoSuchMethodError error) {
159            throw new IllegalStateException(
160                "Request class is not compatible with this HTTP client implementation. Probably a wrong http client package is used. Please try \"commercetools-okhttp-client3\" instead");
161        }
162        return httpRequestBuilder.build();
163    }
164
165    private CompletableFuture<okhttp3.Response> makeRequest(final OkHttpClient client, final okhttp3.Request request) {
166        final okhttp3.Call call = client.newCall(request);
167        final OkHttpResponseFuture result = new OkHttpResponseFuture();
168        call.enqueue(result);
169        return result.future;
170    }
171
172    private static class OkHttpResponseFuture implements okhttp3.Callback {
173        public final CompletableFuture<okhttp3.Response> future = new CompletableFuture<>();
174
175        public OkHttpResponseFuture() {
176        }
177
178        @Override
179        public void onFailure(final okhttp3.Call call, final IOException e) {
180            future.completeExceptionally(e);
181        }
182
183        @Override
184        public void onResponse(final okhttp3.Call call, final okhttp3.Response response) throws IOException {
185            future.complete(response);
186        }
187    }
188
189    @Override
190    public void closeDelegate() throws IOException {
191        okHttpClient.dispatcher().executorService().shutdown();
192        okHttpClient.connectionPool().evictAll();
193        if (okHttpClient.cache() != null)
194            Objects.requireNonNull(okHttpClient.cache()).close();
195    }
196
197    public static class UnzippingInterceptor implements okhttp3.Interceptor {
198        @Override
199        @NotNull
200        public okhttp3.Response intercept(Chain chain) throws IOException {
201            okhttp3.Response response = chain.proceed(chain.request());
202            return unzip(response);
203        }
204
205        private okhttp3.Response unzip(final okhttp3.Response response) throws IOException {
206            if (!"gzip".equalsIgnoreCase(response.header("Content-Encoding"))) {
207                return response;
208            }
209
210            okhttp3.ResponseBody responseBody = response.body();
211            if (responseBody == null) {
212                return response;
213            }
214
215            GzipSource gzipSource = new GzipSource(responseBody.source());
216            okhttp3.Headers strippedHeaders = response.headers()
217                    .newBuilder()
218                    .removeAll("Content-Encoding")
219                    .removeAll("Content-Length")
220                    .build();
221            String contentType = response.header("Content-Type");
222            return response.newBuilder()
223                    .headers(strippedHeaders)
224                    .body(okhttp3.ResponseBody.create(Okio.buffer(gzipSource), okhttp3.MediaType.get(contentType), -1L))
225                    .build();
226        }
227    }
228}