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