001package com.box.sdkgen.networking.defaultnetworkclient; 002 003import static com.box.sdkgen.serialization.json.JsonManager.jsonToSerializedData; 004import static com.box.sdkgen.serialization.json.JsonManager.sdToJson; 005import static com.box.sdkgen.serialization.json.JsonManager.sdToUrlParams; 006import static java.util.Collections.singletonList; 007import static okhttp3.ConnectionSpec.MODERN_TLS; 008 009import com.box.sdkgen.box.errors.BoxSDKError; 010import com.box.sdkgen.networking.fetchoptions.FetchOptions; 011import com.box.sdkgen.networking.fetchoptions.MultipartItem; 012import com.box.sdkgen.networking.fetchoptions.ResponseFormat; 013import com.box.sdkgen.networking.fetchresponse.FetchResponse; 014import com.box.sdkgen.networking.network.NetworkSession; 015import com.box.sdkgen.networking.networkclient.NetworkClient; 016import java.io.IOException; 017import java.util.Locale; 018import java.util.Map; 019import java.util.Objects; 020import java.util.TreeMap; 021import java.util.concurrent.TimeUnit; 022import java.util.stream.Collectors; 023import okhttp3.Call; 024import okhttp3.Headers; 025import okhttp3.HttpUrl; 026import okhttp3.MediaType; 027import okhttp3.MultipartBody; 028import okhttp3.OkHttpClient; 029import okhttp3.Request; 030import okhttp3.RequestBody; 031import okhttp3.Response; 032import okio.BufferedSink; 033import okio.Okio; 034import okio.Source; 035 036public class DefaultNetworkClient implements NetworkClient { 037 038 protected OkHttpClient httpClient; 039 040 public DefaultNetworkClient(OkHttpClient httpClient) { 041 this.httpClient = httpClient; 042 } 043 044 public DefaultNetworkClient() { 045 httpClient = getDefaultOkHttpClientBuilder().build(); 046 } 047 048 public FetchResponse fetch(FetchOptions options) { 049 NetworkSession networkSession = 050 options.getNetworkSession() == null ? new NetworkSession() : options.getNetworkSession(); 051 052 FetchOptions fetchOptions = 053 networkSession.getInterceptors().stream() 054 .reduce( 055 options, 056 (modifiedOptions, interceptor) -> interceptor.beforeRequest(modifiedOptions), 057 (o1, o2) -> o2); 058 059 boolean authenticationNeeded = false; 060 Request request; 061 FetchResponse fetchResponse = null; 062 Exception exceptionThrown = null; 063 064 int attemptNumber = 1; 065 int numberOfRetriesOnException = 0; 066 int attemptForRetry = 0; 067 boolean shouldRetry = false; 068 069 while (true) { 070 request = prepareRequest(fetchOptions, authenticationNeeded, networkSession); 071 072 Response response = null; 073 try { 074 response = executeOnClient(request); 075 076 Map<String, String> headersMap = 077 response.headers().toMultimap().entrySet().stream() 078 .collect( 079 Collectors.toMap( 080 Map.Entry::getKey, 081 e -> e.getValue().get(0), 082 (existing, replacement) -> existing, 083 () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 084 085 String responseUrl = 086 response.networkResponse() != null 087 ? response.networkResponse().request().url().toString() 088 : response.request().url().toString(); 089 090 attemptForRetry = attemptNumber; 091 fetchResponse = 092 Objects.equals(fetchOptions.getResponseFormat().getEnumValue(), ResponseFormat.BINARY) 093 ? new FetchResponse.Builder(response.code(), headersMap) 094 .content(response.body().byteStream()) 095 .url(responseUrl) 096 .build() 097 : new FetchResponse.Builder(response.code(), headersMap) 098 .data( 099 response.body() != null 100 ? jsonToSerializedData(response.body().string()) 101 : null) 102 .url(responseUrl) 103 .build(); 104 105 fetchResponse = 106 networkSession.getInterceptors().stream() 107 .reduce( 108 fetchResponse, 109 (modifiedResponse, interceptor) -> interceptor.afterRequest(modifiedResponse), 110 (o1, o2) -> o2); 111 112 } catch (Exception e) { 113 exceptionThrown = e; 114 numberOfRetriesOnException++; 115 attemptForRetry = numberOfRetriesOnException; 116 fetchResponse = new FetchResponse.Builder(0, new TreeMap<>()).build(); 117 if (response != null) { 118 response.close(); 119 } 120 } 121 122 shouldRetry = 123 networkSession 124 .getRetryStrategy() 125 .shouldRetry(fetchOptions, fetchResponse, attemptForRetry); 126 127 if (shouldRetry) { 128 double retryDelay = 129 networkSession 130 .getRetryStrategy() 131 .retryAfter(fetchOptions, fetchResponse, attemptForRetry); 132 if (retryDelay > 0) { 133 try { 134 TimeUnit.SECONDS.sleep((long) retryDelay); 135 } catch (InterruptedException ie) { 136 Thread.currentThread().interrupt(); 137 throw new BoxSDKError("Retry interrupted", ie); 138 } 139 } 140 attemptNumber++; 141 continue; 142 } 143 144 if (fetchResponse.getStatus() >= 300 145 && fetchResponse.getStatus() < 400 146 && fetchOptions.followRedirects) { 147 if (!fetchResponse.getHeaders().containsKey("Location")) { 148 throw new BoxSDKError( 149 "Redirect response missing Location header for " + fetchOptions.getUrl()); 150 } 151 return fetch( 152 new FetchOptions.Builder(fetchResponse.getHeaders().get("Location"), "GET") 153 .responseFormat(fetchOptions.getResponseFormat()) 154 .auth(fetchOptions.getAuth()) 155 .networkSession(networkSession) 156 .build()); 157 } 158 159 if (fetchResponse != null 160 && fetchResponse.getStatus() >= 200 161 && fetchResponse.getStatus() < 400) { 162 return fetchResponse; 163 } 164 165 throwOnUnsuccessfulResponse(request, fetchResponse, exceptionThrown); 166 } 167 } 168 169 private static Request prepareRequest( 170 FetchOptions options, boolean reauthenticate, NetworkSession networkSession) { 171 Request.Builder requestBuilder = new Request.Builder().url(options.getUrl()); 172 Headers headers = prepareHeaders(options, reauthenticate, networkSession); 173 HttpUrl url = prepareUrl(options); 174 RequestBody body = prepareRequestBody(options); 175 176 requestBuilder.headers(headers); 177 requestBuilder.url(url); 178 requestBuilder.method(options.getMethod().toUpperCase(Locale.ROOT), body); 179 return requestBuilder.build(); 180 } 181 182 private static Headers prepareHeaders( 183 FetchOptions options, boolean reauthenticate, NetworkSession networkSession) { 184 Headers.Builder headersBuilder = new Headers.Builder(); 185 186 networkSession.getAdditionalHeaders().forEach(headersBuilder::add); 187 188 if (options.getHeaders() != null) { 189 options.getHeaders().forEach(headersBuilder::add); 190 } 191 if (options.getAuth() != null) { 192 if (reauthenticate) { 193 options.getAuth().refreshToken(networkSession); 194 } 195 headersBuilder.add( 196 "Authorization", options.getAuth().retrieveAuthorizationHeader(networkSession)); 197 } 198 return headersBuilder.build(); 199 } 200 201 private static HttpUrl prepareUrl(FetchOptions options) { 202 203 HttpUrl baseUrl = HttpUrl.parse(options.getUrl()); 204 if (baseUrl == null) { 205 throw new IllegalArgumentException("Invalid URL " + options.getUrl()); 206 } 207 HttpUrl.Builder urlBuilder = baseUrl.newBuilder(); 208 if (options.getParams() != null) { 209 options.getParams().forEach(urlBuilder::addQueryParameter); 210 } 211 return urlBuilder.build(); 212 } 213 214 private static RequestBody prepareRequestBody(FetchOptions options) { 215 if (options.getMethod().equalsIgnoreCase("GET")) { 216 return null; 217 } 218 String contentType = options.getContentType(); 219 MediaType mediaType = MediaType.parse(contentType); 220 switch (contentType) { 221 case "application/json": 222 case "application/json-patch+json": 223 return options.getData() != null 224 ? RequestBody.create(sdToJson(options.getData()), mediaType) 225 : RequestBody.create("", mediaType); 226 case "application/x-www-form-urlencoded": 227 return options.getData() != null 228 ? RequestBody.create(sdToUrlParams(options.getData()), mediaType) 229 : RequestBody.create("", mediaType); 230 case "multipart/form-data": 231 MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); 232 for (MultipartItem part : options.multipartData) { 233 if (part.getData() != null) { 234 bodyBuilder.addFormDataPart(part.getPartName(), sdToJson(part.getData())); 235 } else { 236 bodyBuilder.addFormDataPart( 237 part.getPartName(), 238 part.getFileName() != null ? part.getFileName() : "file", 239 createMultipartRequestBody(part)); 240 } 241 } 242 return bodyBuilder.build(); 243 default: 244 throw new IllegalArgumentException("Unsupported content type " + contentType); 245 } 246 } 247 248 protected Call createNewCall(Request request) { 249 return this.httpClient.newCall(request); 250 } 251 252 private Response executeOnClient(Request request) { 253 try { 254 return createNewCall(request).execute(); 255 } catch (IOException e) { 256 throw new RuntimeException(e); 257 } 258 } 259 260 public static RequestBody createMultipartRequestBody(MultipartItem part) { 261 return new RequestBody() { 262 @Override 263 public MediaType contentType() { 264 if (part.contentType != null) { 265 return MediaType.parse(part.contentType); 266 } 267 return MediaType.parse("application/octet-stream"); 268 } 269 270 @Override 271 public void writeTo(BufferedSink sink) throws IOException { 272 try (Source source = Okio.source(part.getFileStream())) { 273 sink.writeAll(source); 274 } 275 } 276 }; 277 } 278 279 public static OkHttpClient.Builder getDefaultOkHttpClientBuilder() { 280 return new OkHttpClient.Builder() 281 .followSslRedirects(true) 282 .followRedirects(false) 283 .connectionSpecs(singletonList(MODERN_TLS)) 284 .retryOnConnectionFailure(false); 285 } 286 287 private static void throwOnUnsuccessfulResponse( 288 Request request, FetchResponse fetchResponse, Exception exceptionThrown) { 289 if (fetchResponse == null) { 290 throw new RuntimeException(exceptionThrown.getMessage(), exceptionThrown); 291 } 292 throw new RuntimeException(fetchResponse.getData().toString()); 293 } 294}