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}