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}