001package com.box.sdk;
002
003import java.io.ByteArrayInputStream;
004import java.io.IOException;
005import java.io.InputStream;
006import java.io.OutputStream;
007import java.net.HttpURLConnection;
008import java.net.MalformedURLException;
009import java.net.ProtocolException;
010import java.net.URL;
011import java.security.KeyManagementException;
012import java.security.NoSuchAlgorithmException;
013import java.util.ArrayList;
014import java.util.List;
015import java.util.Map;
016import java.util.logging.Level;
017import java.util.logging.Logger;
018import javax.net.ssl.HttpsURLConnection;
019import javax.net.ssl.SSLContext;
020import javax.net.ssl.SSLParameters;
021import javax.net.ssl.SSLSocketFactory;
022
023import com.box.sdk.http.HttpHeaders;
024import com.box.sdk.http.HttpMethod;
025import com.eclipsesource.json.JsonObject;
026
027
028/**
029* Used to make HTTP requests to the Box API.
030*
031* <p>All requests to the REST API are sent using this class or one of its subclasses. This class wraps {@link
032* HttpURLConnection} in order to provide a simpler interface that can automatically handle various conditions specific
033* to Box's API. Requests will be authenticated using a {@link BoxAPIConnection} (if one is provided), so it isn't
034* necessary to add authorization headers. Requests can also be sent more than once, unlike with HttpURLConnection. If
035* an error occurs while sending a request, it will be automatically retried (with a back off delay) up to the maximum
036* number of times set in the BoxAPIConnection.</p>
037*
038* <p>Specifying a body for a BoxAPIRequest is done differently than it is with HttpURLConnection. Instead of writing to
039* an OutputStream, the request is provided an {@link InputStream} which will be read when the {@link #send} method is
040* called. This makes it easy to retry requests since the stream can automatically reset and reread with each attempt.
041* If the stream cannot be reset, then a new stream will need to be provided before each call to send. There is also a
042* convenience method for specifying the body as a String, which simply wraps the String with an InputStream.</p>
043*/
044public class BoxAPIRequest {
045    private static final Logger LOGGER = Logger.getLogger(BoxAPIRequest.class.getName());
046    private static final int BUFFER_SIZE = 8192;
047    private static final int MAX_REDIRECTS = 3;
048    private static SSLSocketFactory sslSocketFactory;
049
050    private final BoxAPIConnection api;
051    private final List<RequestHeader> headers;
052    private final String method;
053
054    private URL url;
055    private BackoffCounter backoffCounter;
056    private int connectTimeout;
057    private int readTimeout;
058    private InputStream body;
059    private long bodyLength;
060    private Map<String, List<String>> requestProperties;
061    private int numRedirects;
062    private boolean followRedirects = true;
063    private boolean shouldAuthenticate;
064
065    static {
066        // Setup the SSL context manually to force newer TLS version on legacy Java environments
067        // This is necessary because Java 7 uses TLSv1.0 by default, but the Box API will need
068        // to deprecate this protocol in the future.  To prevent clients from breaking, we must
069        // ensure that they are using TLSv1.1 or greater!
070        SSLContext sc = null;
071        try {
072            sc = SSLContext.getDefault();
073            SSLParameters params = sc.getDefaultSSLParameters();
074            boolean supportsNewTLS = false;
075            for (String protocol : params.getProtocols()) {
076                if (protocol.compareTo("TLSv1") > 0) {
077                    supportsNewTLS = true;
078                    break;
079                }
080            }
081            if (!supportsNewTLS) {
082                // Try to upgrade to a higher TLS version
083                sc = null;
084                sc = SSLContext.getInstance("TLSv1.1");
085                sc.init(null, null, new java.security.SecureRandom());
086                sc = SSLContext.getInstance("TLSv1.2");
087                sc.init(null, null, new java.security.SecureRandom());
088            }
089        } catch (NoSuchAlgorithmException ex) {
090            if (sc == null) {
091                LOGGER.warning("Unable to set up SSL context for HTTPS!  This may result in the inability "
092                    + " to connect to the Box API.");
093            }
094            if (sc != null && sc.getProtocol().equals("TLSv1")) {
095                // Could not find a good version of TLS
096                LOGGER.warning("Using deprecated TLSv1 protocol, which will be deprecated by the Box API!  Upgrade "
097                    + "to a newer version of Java as soon as possible.");
098            }
099        } catch (KeyManagementException ex) {
100            LOGGER.warning("Exception when initializing SSL Context!  This may result in the inabilty to connect to "
101                + "the Box API");
102            sc = null;
103        }
104
105        if (sc != null) {
106            sslSocketFactory = sc.getSocketFactory();
107        }
108
109    }
110
111    /**
112     * Constructs an unauthenticated BoxAPIRequest.
113     * @param  url    the URL of the request.
114     * @param  method the HTTP method of the request.
115     */
116    public BoxAPIRequest(URL url, String method) {
117        this(null, url, method);
118    }
119
120    /**
121     * Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection.
122     * @param  api    an API connection for authenticating the request.
123     * @param  url    the URL of the request.
124     * @param  method the HTTP method of the request.
125     */
126    public BoxAPIRequest(BoxAPIConnection api, URL url, String method) {
127        this.api = api;
128        this.url = url;
129        this.method = method;
130        this.headers = new ArrayList<RequestHeader>();
131        if (api != null) {
132            Map<String, String> customHeaders = api.getHeaders();
133            if (customHeaders != null) {
134                for (String header : customHeaders.keySet()) {
135                    this.addHeader(header, customHeaders.get(header));
136                }
137            }
138            this.headers.add(new RequestHeader("X-Box-UA", api.getBoxUAHeader()));
139        }
140        this.backoffCounter = new BackoffCounter(new Time());
141        this.shouldAuthenticate = true;
142        if (api != null) {
143            this.connectTimeout = api.getConnectTimeout();
144            this.readTimeout = api.getReadTimeout();
145        } else {
146            this.connectTimeout = BoxGlobalSettings.getConnectTimeout();
147            this.readTimeout = BoxGlobalSettings.getReadTimeout();
148        }
149
150        this.addHeader("Accept-Encoding", "gzip");
151        this.addHeader("Accept-Charset", "utf-8");
152
153    }
154
155    /**
156     * Constructs an authenticated BoxAPIRequest using a provided BoxAPIConnection.
157     * @param  api    an API connection for authenticating the request.
158     * @param  url the URL of the request.
159     * @param  method the HTTP method of the request.
160     */
161    public BoxAPIRequest(BoxAPIConnection api, URL url, HttpMethod method) {
162        this(api, url, method.name());
163    }
164
165    /**
166     * Constructs an request, using URL and HttpMethod.
167     * @param  url the URL of the request.
168     * @param  method the HTTP method of the request.
169     */
170    public BoxAPIRequest(URL url, HttpMethod method) {
171        this(url, method.name());
172    }
173
174    /**
175     * Adds an HTTP header to this request.
176     * @param key   the header key.
177     * @param value the header value.
178     */
179    public void addHeader(String key, String value) {
180        if (key.equals("As-User")) {
181            for (int i = 0; i < this.headers.size(); i++) {
182                if (this.headers.get(i).getKey().equals("As-User")) {
183                    this.headers.remove(i);
184                }
185            }
186        }
187        if (key.equals("X-Box-UA")) {
188            throw new IllegalArgumentException("Altering the X-Box-UA header is not permitted");
189        }
190        this.headers.add(new RequestHeader(key, value));
191    }
192
193    /**
194     * Sets a Connect timeout for this request in milliseconds.
195     * @param timeout the timeout in milliseconds.
196     */
197    public void setConnectTimeout(int timeout) {
198        this.connectTimeout = timeout;
199    }
200
201    /**
202     * Gets the connect timeout for the request.
203     * @return the request connection timeout.
204     */
205    public int getConnectTimeout() {
206        return this.connectTimeout;
207    }
208
209    /**
210     * Sets a read timeout for this request in milliseconds.
211     * @param timeout the timeout in milliseconds.
212     */
213    public void setReadTimeout(int timeout) {
214        this.readTimeout = timeout;
215    }
216
217    /**
218     * Gets the read timeout for the request.
219     * @return the request's read timeout.
220     */
221    public int getReadTimeout() {
222        return this.readTimeout;
223    }
224
225    /**
226     * Sets whether or not to follow redirects (i.e. Location header)
227     * @param followRedirects true to follow, false to not follow
228     */
229    public void setFollowRedirects(boolean followRedirects) {
230        this.followRedirects = followRedirects;
231    }
232
233    /**
234     * Gets the stream containing contents of this request's body.
235     *
236     * <p>Note that any bytes that read from the returned stream won't be sent unless the stream is reset back to its
237     * initial position.</p>
238     *
239     * @return an InputStream containing the contents of this request's body.
240     */
241    public InputStream getBody() {
242        return this.body;
243    }
244
245    /**
246     * Sets the request body to the contents of an InputStream.
247     *
248     * <p>The stream must support the {@link InputStream#reset} method if auto-retry is used or if the request needs to
249     * be resent. Otherwise, the body must be manually set before each call to {@link #send}.</p>
250     *
251     * @param stream an InputStream containing the contents of the body.
252     */
253    public void setBody(InputStream stream) {
254        this.body = stream;
255    }
256
257    /**
258     * Sets the request body to the contents of an InputStream.
259     *
260     * <p>Providing the length of the InputStream allows for the progress of the request to be monitored when calling
261     * {@link #send(ProgressListener)}.</p>
262     *
263     * <p> See {@link #setBody(InputStream)} for more information on setting the body of the request.</p>
264     *
265     * @param stream an InputStream containing the contents of the body.
266     * @param length the expected length of the stream.
267     */
268    public void setBody(InputStream stream, long length) {
269        this.bodyLength = length;
270        this.body = stream;
271    }
272
273    /**
274     * Sets the request body to the contents of a String.
275     *
276     * <p>If the contents of the body are large, then it may be more efficient to use an {@link InputStream} instead of
277     * a String. Using a String requires that the entire body be in memory before sending the request.</p>
278     *
279     * @param body a String containing the contents of the body.
280     */
281    public void setBody(String body) {
282        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
283        this.bodyLength = bytes.length;
284        this.body = new ByteArrayInputStream(bytes);
285    }
286
287    /**
288     * Gets the URL from the request.
289     *
290     * @return a URL containing the URL of the request.
291     */
292    public URL getUrl() {
293        return this.url;
294    }
295
296    /**
297     * Gets the http method from the request.
298     *
299     * @return http method
300     */
301    public String getMethod() {
302        return this.method;
303    }
304
305    /**
306     * Get headers as list of RequestHeader objects.
307     * @return headers as list of RequestHeader objects
308     */
309    protected List<RequestHeader> getHeaders() {
310        return this.headers;
311    }
312
313    /**
314     * Sends this request and returns a BoxAPIResponse containing the server's response.
315     *
316     * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it
317     * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response,
318     * then it can be cast to a {@link BoxJSONResponse} like so:</p>
319     *
320     * <pre>BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry();</pre>
321     *
322     * @throws BoxAPIException if the server returns an error code or if a network error occurs.
323     * @return a {@link BoxAPIResponse} containing the server's response.
324     */
325    public BoxAPIResponse sendWithoutRetry() {
326        return this.trySend(null);
327    }
328
329    /**
330     * Sends this request and returns a BoxAPIResponse containing the server's response.
331     *
332     * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it
333     * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response,
334     * then it can be cast to a {@link BoxJSONResponse} like so:</p>
335     *
336     * <pre>BoxJSONResponse response = (BoxJSONResponse) request.send();</pre>
337     *
338     * <p>If the server returns an error code or if a network error occurs, then the request will be automatically
339     * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException}
340     * will be thrown.</p>
341     *
342     * <p> See {@link #send} for more information on sending requests.</p>
343     *
344     * @throws BoxAPIException if the server returns an error code or if a network error occurs.
345     * @return a {@link BoxAPIResponse} containing the server's response.
346     */
347    public BoxAPIResponse send() {
348        return this.send(null);
349    }
350
351    /**
352     * Sends this request while monitoring its progress and returns a BoxAPIResponse containing the server's response.
353     *
354     * <p>The type of the returned BoxAPIResponse will be based on the content type returned by the server, allowing it
355     * to be cast to a more specific type. For example, if it's known that the API call will return a JSON response,
356     * then it can be cast to a {@link BoxJSONResponse} like so:</p>
357     *
358     * <p>If the server returns an error code or if a network error occurs, then the request will be automatically
359     * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException}
360     * will be thrown.</p>
361     *
362     * <p>A ProgressListener is generally only useful when the size of the request is known beforehand. If the size is
363     * unknown, then the ProgressListener will be updated for each byte sent, but the total number of bytes will be
364     * reported as 0.</p>
365     *
366     * <p> See {@link #send} for more information on sending requests.</p>
367     *
368     * @param  listener a listener for monitoring the progress of the request.
369     * @throws BoxAPIException if the server returns an error code or if a network error occurs.
370     * @return a {@link BoxAPIResponse} containing the server's response.
371     */
372    public BoxAPIResponse send(ProgressListener listener) {
373        if (this.api == null) {
374            this.backoffCounter.reset(BoxGlobalSettings.getMaxRetryAttempts() + 1);
375        } else {
376            this.backoffCounter.reset(this.api.getMaxRetryAttempts() + 1);
377        }
378
379        while (this.backoffCounter.getAttemptsRemaining() > 0) {
380            try {
381                return this.trySend(listener);
382            } catch (BoxAPIException apiException) {
383                if (!this.backoffCounter.decrement()
384                    || !isResponseRetryable(apiException.getResponseCode(), apiException)) {
385                    throw apiException;
386                }
387
388                LOGGER.log(Level.WARNING, "Retrying request due to transient error status={0} body={1}",
389                        new Object[] {apiException.getResponseCode(), apiException.getResponse()});
390
391                try {
392                    this.resetBody();
393                } catch (IOException ioException) {
394                    throw apiException;
395                }
396
397                try {
398                    List<String> retryAfterHeader = apiException.getHeaders().get("Retry-After");
399                    if (retryAfterHeader == null) {
400                        this.backoffCounter.waitBackoff();
401                    } else {
402                        int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0));
403                        this.backoffCounter.waitBackoff(retryAfterDelay);
404                    }
405                } catch (InterruptedException interruptedException) {
406                    Thread.currentThread().interrupt();
407                    throw apiException;
408                }
409            }
410        }
411
412        throw new RuntimeException();
413    }
414
415    /**
416      * Sends a request to upload a file part and returns a BoxFileUploadSessionPart containing information
417      * about the upload part. This method is separate from send() because it has custom retry logic.
418      *
419      * <p>If the server returns an error code or if a network error occurs, then the request will be automatically
420      * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException}
421      * will be thrown.</p>
422      *
423      * @param session The BoxFileUploadSession uploading the part
424      * @param offset Offset of the part being uploaded
425      * @throws BoxAPIException if the server returns an error code or if a network error occurs.
426      * @return A {@link BoxFileUploadSessionPart} part that has been uploaded.
427      */
428    BoxFileUploadSessionPart sendForUploadPart(BoxFileUploadSession session, long offset) {
429        if (this.api == null) {
430            this.backoffCounter.reset(BoxGlobalSettings.getMaxRetryAttempts() + 1);
431        } else {
432            this.backoffCounter.reset(this.api.getMaxRetryAttempts() + 1);
433        }
434
435        while (this.backoffCounter.getAttemptsRemaining() > 0) {
436            try {
437                BoxJSONResponse response = (BoxJSONResponse) this.trySend(null);
438                JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
439                return new BoxFileUploadSessionPart((JsonObject) jsonObject.get("part"));
440            } catch (BoxAPIException apiException) {
441                if (!this.backoffCounter.decrement()
442                    || !isResponseRetryable(apiException.getResponseCode(), apiException)) {
443                    throw apiException;
444                }
445                if (apiException.getResponseCode() == 500) {
446                    try {
447                        Iterable<BoxFileUploadSessionPart> parts = session.listParts();
448                        for (BoxFileUploadSessionPart part : parts) {
449                            if (part.getOffset() == offset) {
450                                return part;
451                            }
452                        }
453                    } catch (BoxAPIException e) { }
454                }
455                LOGGER.log(Level.WARNING, "Retrying request due to transient error status={0} body={1}",
456                    new Object[] {apiException.getResponseCode(), apiException.getResponse()});
457
458                try {
459                    this.resetBody();
460                } catch (IOException ioException) {
461                    throw apiException;
462                }
463
464                try {
465                    this.backoffCounter.waitBackoff();
466                } catch (InterruptedException interruptedException) {
467                    Thread.currentThread().interrupt();
468                    throw apiException;
469                }
470            }
471        }
472
473        throw new RuntimeException();
474    }
475
476    /**
477     * Returns a String containing the URL, HTTP method, headers and body of this request.
478     * @return a String containing information about this request.
479     */
480    @Override
481    public String toString() {
482        String lineSeparator = System.getProperty("line.separator");
483        StringBuilder builder = new StringBuilder();
484        builder.append("Request");
485        builder.append(lineSeparator);
486        builder.append(this.method);
487        builder.append(' ');
488        builder.append(this.url.toString());
489        builder.append(lineSeparator);
490
491        if (this.requestProperties != null) {
492
493            for (Map.Entry<String, List<String>> entry : this.requestProperties.entrySet()) {
494                List<String> nonEmptyValues = new ArrayList<String>();
495                for (String value : entry.getValue()) {
496                    if (value != null && value.trim().length() != 0) {
497                        nonEmptyValues.add(value);
498                    }
499                }
500
501                if (nonEmptyValues.size() == 0) {
502                    continue;
503                }
504
505                builder.append(entry.getKey());
506                builder.append(": ");
507                for (String value : nonEmptyValues) {
508                    builder.append(value);
509                    builder.append(", ");
510                }
511
512                builder.delete(builder.length() - 2, builder.length());
513                builder.append(lineSeparator);
514            }
515        }
516
517        String bodyString = this.bodyToString();
518        if (bodyString != null) {
519            builder.append(lineSeparator);
520            builder.append(bodyString);
521        }
522
523        return builder.toString().trim();
524    }
525
526    /**
527     * Returns a String representation of this request's body used in {@link #toString}. This method returns
528     * null by default.
529     *
530     * <p>A subclass may want override this method if the body can be converted to a String for logging or debugging
531     * purposes.</p>
532     *
533     * @return a String representation of this request's body.
534     */
535    protected String bodyToString() {
536        return null;
537    }
538
539    /**
540     * Writes the body of this request to an HttpURLConnection.
541     *
542     * <p>Subclasses overriding this method must remember to close the connection's OutputStream after writing.</p>
543     *
544     * @param connection the connection to which the body should be written.
545     * @param listener   an optional listener for monitoring the write progress.
546     * @throws BoxAPIException if an error occurs while writing to the connection.
547     */
548    protected void writeBody(HttpURLConnection connection, ProgressListener listener) {
549        if (this.body == null) {
550            return;
551        }
552
553        connection.setDoOutput(true);
554        try {
555            OutputStream output = connection.getOutputStream();
556            if (listener != null) {
557                output = new ProgressOutputStream(output, listener, this.bodyLength);
558            }
559            int b = this.body.read();
560            while (b != -1) {
561                output.write(b);
562                b = this.body.read();
563            }
564            output.close();
565        } catch (IOException e) {
566            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
567        }
568    }
569
570    /**
571     * Resets the InputStream containing this request's body.
572     *
573     * <p>This method will be called before each attempt to resend the request, giving subclasses an opportunity to
574     * reset any streams that need to be read when sending the body.</p>
575     *
576     * @throws IOException if the stream cannot be reset.
577     */
578    protected void resetBody() throws IOException {
579        if (this.body != null) {
580            this.body.reset();
581        }
582    }
583
584    void setBackoffCounter(BackoffCounter counter) {
585        this.backoffCounter = counter;
586    }
587
588    private BoxAPIResponse trySend(ProgressListener listener) {
589        if (this.api != null) {
590            RequestInterceptor interceptor = this.api.getRequestInterceptor();
591            if (interceptor != null) {
592                BoxAPIResponse response = interceptor.onRequest(this);
593                if (response != null) {
594                    return response;
595                }
596            }
597        }
598
599        HttpURLConnection connection = this.createConnection();
600
601        if (connection instanceof HttpsURLConnection) {
602            HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
603
604            if (sslSocketFactory != null) {
605                httpsConnection.setSSLSocketFactory(sslSocketFactory);
606            }
607        }
608
609        if (this.bodyLength > 0) {
610            connection.setFixedLengthStreamingMode((int) this.bodyLength);
611            connection.setDoOutput(true);
612        }
613
614        if (this.api != null) {
615            if (this.shouldAuthenticate) {
616                connection.addRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + this.api.lockAccessToken());
617            }
618            connection.setRequestProperty("User-Agent", this.api.getUserAgent());
619            if (this.api.getProxy() != null) {
620                if (this.api.getProxyUsername() != null && this.api.getProxyPassword() != null) {
621                    String usernameAndPassword = this.api.getProxyUsername() + ":" + this.api.getProxyPassword();
622                    String encoded = new String(Base64.encode(usernameAndPassword.getBytes()));
623                    connection.addRequestProperty("Proxy-Authorization", "Basic " + encoded);
624                }
625            }
626
627            if (this.api instanceof SharedLinkAPIConnection) {
628                SharedLinkAPIConnection sharedItemAPI = (SharedLinkAPIConnection) this.api;
629                String sharedLink = sharedItemAPI.getSharedLink();
630                String boxAPIValue = "shared_link=" + sharedLink;
631                String sharedLinkPassword = sharedItemAPI.getSharedLinkPassword();
632                if (sharedLinkPassword != null) {
633                    boxAPIValue += "&shared_link_password=" + sharedLinkPassword;
634                }
635                connection.addRequestProperty("BoxApi", boxAPIValue);
636            }
637        }
638
639        this.requestProperties = connection.getRequestProperties();
640
641        int responseCode;
642        try {
643            this.writeBody(connection, listener);
644
645            // Ensure that we're connected in case writeBody() didn't write anything.
646            try {
647                connection.connect();
648            } catch (IOException e) {
649                throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
650            }
651
652            this.logRequest(connection);
653
654            // We need to manually handle redirects by creating a new HttpURLConnection so that connection pooling
655            // happens correctly. There seems to be a bug in Oracle's Java implementation where automatically handled
656            // redirects will not keep the connection alive.
657            try {
658                responseCode = connection.getResponseCode();
659            } catch (IOException e) {
660                throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
661            }
662        } finally {
663            if (this.api != null && this.shouldAuthenticate) {
664                this.api.unlockAccessToken();
665            }
666        }
667
668        if (isResponseRedirect(responseCode)) {
669            return this.handleRedirect(connection, listener);
670        }
671
672        String contentType = connection.getContentType();
673        BoxAPIResponse response;
674        if (contentType == null) {
675            response = new BoxAPIResponse(connection);
676        } else if (contentType.contains("application/json")) {
677            response = new BoxJSONResponse(connection);
678        } else {
679            response = new BoxAPIResponse(connection);
680        }
681
682        return response;
683    }
684
685    private BoxAPIResponse handleRedirect(HttpURLConnection connection, ProgressListener listener) {
686        if (this.numRedirects >= MAX_REDIRECTS) {
687            throw new BoxAPIException("The Box API responded with too many redirects.");
688        }
689        this.numRedirects++;
690
691        // Even though the redirect response won't have a body, we need to read the InputStream so that Java will put
692        // the connection back in the connection pool.
693        try {
694            InputStream stream = connection.getInputStream();
695            byte[] buffer = new byte[8192];
696            int n = stream.read(buffer);
697            while (n != -1) {
698                n = stream.read(buffer);
699            }
700            stream.close();
701        } catch (IOException e) {
702            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
703        }
704
705        String redirect = connection.getHeaderField("Location");
706        try {
707            this.url = new URL(redirect);
708        } catch (MalformedURLException e) {
709            throw new BoxAPIException("The Box API responded with an invalid redirect.", e);
710        }
711
712        if (this.followRedirects) {
713            return this.trySend(listener);
714        } else {
715            BoxRedirectResponse redirectResponse = new BoxRedirectResponse();
716            redirectResponse.setRedirectURL(this.url);
717            return redirectResponse;
718        }
719    }
720
721    private void logRequest(HttpURLConnection connection) {
722        if (LOGGER.isLoggable(Level.FINE)) {
723            LOGGER.log(Level.FINE, this.toString());
724        }
725    }
726
727    private HttpURLConnection createConnection() {
728        HttpURLConnection connection = null;
729
730        try {
731            if (this.api == null || this.api.getProxy() == null) {
732                connection = (HttpURLConnection) this.url.openConnection();
733            } else {
734                connection = (HttpURLConnection) this.url.openConnection(this.api.getProxy());
735            }
736        } catch (IOException e) {
737            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
738        }
739
740        try {
741            connection.setRequestMethod(this.method);
742        } catch (ProtocolException e) {
743            throw new BoxAPIException("Couldn't connect to the Box API because the request's method was invalid.", e);
744        }
745
746        connection.setConnectTimeout(this.connectTimeout);
747        connection.setReadTimeout(this.readTimeout);
748
749        // Don't allow HttpURLConnection to automatically redirect because it messes up the connection pool. See the
750        // trySend(ProgressListener) method for how we handle redirects.
751        connection.setInstanceFollowRedirects(false);
752
753        for (RequestHeader header : this.headers) {
754            connection.addRequestProperty(header.getKey(), header.getValue());
755        }
756
757        return connection;
758    }
759
760    void shouldAuthenticate(boolean shouldAuthenticate) {
761        this.shouldAuthenticate = shouldAuthenticate;
762    }
763
764    /**
765     *
766     * @param  responseCode HTTP error code of the response
767     * @param  apiException BoxAPIException thrown
768     * @return true if the response is one that should be retried, otherwise false
769     */
770    public static boolean isResponseRetryable(int responseCode, BoxAPIException apiException) {
771        String response = apiException.getResponse();
772        String message = apiException.getMessage();
773        String errorCode = "";
774
775        try {
776            JsonObject responseBody = JsonObject.readFrom(response);
777            if (responseBody.get("code") != null) {
778                errorCode = responseBody.get("code").toString();
779            }
780        } catch (Exception e) { }
781
782        Boolean isClockSkewError =  responseCode == 400
783                                    && errorCode.contains("invalid_grant")
784                                    && message.contains("exp");
785        return (isClockSkewError || responseCode >= 500 || responseCode == 429);
786    }
787    private static boolean isResponseRedirect(int responseCode) {
788        return (responseCode == 301 || responseCode == 302);
789    }
790
791    /**
792     * Class for mapping a request header and value.
793     */
794    public final class RequestHeader {
795        private final String key;
796        private final String value;
797
798        /**
799         * Construct a request header from header key and value.
800         * @param key header name
801         * @param value header value
802         */
803        public RequestHeader(String key, String value) {
804            this.key = key;
805            this.value = value;
806        }
807
808        /**
809         * Get header key.
810         * @return http header name
811         */
812        public String getKey() {
813            return this.key;
814        }
815
816        /**
817         * Get header value.
818         * @return http header value
819         */
820        public String getValue() {
821            return this.value;
822        }
823    }
824}