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.getMaxRequestAttempts());
375        } else {
376            this.backoffCounter.reset(this.api.getMaxRequestAttempts());
377        }
378
379        while (this.backoffCounter.getAttemptsRemaining() > 0) {
380            try {
381                return this.trySend(listener);
382            } catch (BoxAPIException apiException) {
383                if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) {
384                    throw apiException;
385                }
386
387                LOGGER.log(Level.WARNING, "Retrying request due to transient error status={0} body={1}",
388                        new Object[] {apiException.getResponseCode(), apiException.getResponse()});
389
390                try {
391                    this.resetBody();
392                } catch (IOException ioException) {
393                    throw apiException;
394                }
395
396                try {
397                    this.backoffCounter.waitBackoff();
398                } catch (InterruptedException interruptedException) {
399                    Thread.currentThread().interrupt();
400                    throw apiException;
401                }
402            }
403        }
404
405        throw new RuntimeException();
406    }
407
408    /**
409      * Sends a request to upload a file part and returns a BoxFileUploadSessionPart containing information
410      * about the upload part. This method is separate from send() because it has custom retry logic.
411      *
412      * <p>If the server returns an error code or if a network error occurs, then the request will be automatically
413      * retried. If the maximum number of retries is reached and an error still occurs, then a {@link BoxAPIException}
414      * will be thrown.</p>
415      *
416      * @param session The BoxFileUploadSession uploading the part
417      * @param offset Offset of the part being uploaded
418      * @throws BoxAPIException if the server returns an error code or if a network error occurs.
419      * @return A {@link BoxFileUploadSessionPart} part that has been uploaded.
420      */
421    BoxFileUploadSessionPart sendForUploadPart(BoxFileUploadSession session, long offset) {
422        if (this.api == null) {
423            this.backoffCounter.reset(BoxGlobalSettings.getMaxRequestAttempts());
424        } else {
425            this.backoffCounter.reset(this.api.getMaxRequestAttempts());
426        }
427
428        while (this.backoffCounter.getAttemptsRemaining() > 0) {
429            try {
430                BoxJSONResponse response = (BoxJSONResponse) this.trySend(null);
431                JsonObject jsonObject = JsonObject.readFrom(response.getJSON());
432                return new BoxFileUploadSessionPart((JsonObject) jsonObject.get("part"));
433            } catch (BoxAPIException apiException) {
434                if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) {
435                    throw apiException;
436                }
437                if (apiException.getResponseCode() == 500) {
438                    try {
439                        Iterable<BoxFileUploadSessionPart> parts = session.listParts();
440                        for (BoxFileUploadSessionPart part : parts) {
441                            if (part.getOffset() == offset) {
442                                return part;
443                            }
444                        }
445                    } catch (BoxAPIException e) { }
446                }
447                LOGGER.log(Level.WARNING, "Retrying request due to transient error status={0} body={1}",
448                    new Object[] {apiException.getResponseCode(), apiException.getResponse()});
449
450                try {
451                    this.resetBody();
452                } catch (IOException ioException) {
453                    throw apiException;
454                }
455
456                try {
457                    this.backoffCounter.waitBackoff();
458                } catch (InterruptedException interruptedException) {
459                    Thread.currentThread().interrupt();
460                    throw apiException;
461                }
462            }
463        }
464
465        throw new RuntimeException();
466    }
467
468    /**
469     * Returns a String containing the URL, HTTP method, headers and body of this request.
470     * @return a String containing information about this request.
471     */
472    @Override
473    public String toString() {
474        String lineSeparator = System.getProperty("line.separator");
475        StringBuilder builder = new StringBuilder();
476        builder.append("Request");
477        builder.append(lineSeparator);
478        builder.append(this.method);
479        builder.append(' ');
480        builder.append(this.url.toString());
481        builder.append(lineSeparator);
482
483        if (this.requestProperties != null) {
484
485            for (Map.Entry<String, List<String>> entry : this.requestProperties.entrySet()) {
486                List<String> nonEmptyValues = new ArrayList<String>();
487                for (String value : entry.getValue()) {
488                    if (value != null && value.trim().length() != 0) {
489                        nonEmptyValues.add(value);
490                    }
491                }
492
493                if (nonEmptyValues.size() == 0) {
494                    continue;
495                }
496
497                builder.append(entry.getKey());
498                builder.append(": ");
499                for (String value : nonEmptyValues) {
500                    builder.append(value);
501                    builder.append(", ");
502                }
503
504                builder.delete(builder.length() - 2, builder.length());
505                builder.append(lineSeparator);
506            }
507        }
508
509        String bodyString = this.bodyToString();
510        if (bodyString != null) {
511            builder.append(lineSeparator);
512            builder.append(bodyString);
513        }
514
515        return builder.toString().trim();
516    }
517
518    /**
519     * Returns a String representation of this request's body used in {@link #toString}. This method returns
520     * null by default.
521     *
522     * <p>A subclass may want override this method if the body can be converted to a String for logging or debugging
523     * purposes.</p>
524     *
525     * @return a String representation of this request's body.
526     */
527    protected String bodyToString() {
528        return null;
529    }
530
531    /**
532     * Writes the body of this request to an HttpURLConnection.
533     *
534     * <p>Subclasses overriding this method must remember to close the connection's OutputStream after writing.</p>
535     *
536     * @param connection the connection to which the body should be written.
537     * @param listener   an optional listener for monitoring the write progress.
538     * @throws BoxAPIException if an error occurs while writing to the connection.
539     */
540    protected void writeBody(HttpURLConnection connection, ProgressListener listener) {
541        if (this.body == null) {
542            return;
543        }
544
545        connection.setDoOutput(true);
546        try {
547            OutputStream output = connection.getOutputStream();
548            if (listener != null) {
549                output = new ProgressOutputStream(output, listener, this.bodyLength);
550            }
551            int b = this.body.read();
552            while (b != -1) {
553                output.write(b);
554                b = this.body.read();
555            }
556            output.close();
557        } catch (IOException e) {
558            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
559        }
560    }
561
562    /**
563     * Resets the InputStream containing this request's body.
564     *
565     * <p>This method will be called before each attempt to resend the request, giving subclasses an opportunity to
566     * reset any streams that need to be read when sending the body.</p>
567     *
568     * @throws IOException if the stream cannot be reset.
569     */
570    protected void resetBody() throws IOException {
571        if (this.body != null) {
572            this.body.reset();
573        }
574    }
575
576    void setBackoffCounter(BackoffCounter counter) {
577        this.backoffCounter = counter;
578    }
579
580    private BoxAPIResponse trySend(ProgressListener listener) {
581        if (this.api != null) {
582            RequestInterceptor interceptor = this.api.getRequestInterceptor();
583            if (interceptor != null) {
584                BoxAPIResponse response = interceptor.onRequest(this);
585                if (response != null) {
586                    return response;
587                }
588            }
589        }
590
591        HttpURLConnection connection = this.createConnection();
592
593        if (connection instanceof HttpsURLConnection) {
594            HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;
595
596            if (sslSocketFactory != null) {
597                httpsConnection.setSSLSocketFactory(sslSocketFactory);
598            }
599        }
600
601        if (this.bodyLength > 0) {
602            connection.setFixedLengthStreamingMode((int) this.bodyLength);
603            connection.setDoOutput(true);
604        }
605
606        if (this.api != null) {
607            if (this.shouldAuthenticate) {
608                connection.addRequestProperty(HttpHeaders.AUTHORIZATION, "Bearer " + this.api.lockAccessToken());
609            }
610            connection.setRequestProperty("User-Agent", this.api.getUserAgent());
611            if (this.api.getProxy() != null) {
612                if (this.api.getProxyUsername() != null && this.api.getProxyPassword() != null) {
613                    String usernameAndPassword = this.api.getProxyUsername() + ":" + this.api.getProxyPassword();
614                    String encoded = new String(Base64.encode(usernameAndPassword.getBytes()));
615                    connection.addRequestProperty("Proxy-Authorization", "Basic " + encoded);
616                }
617            }
618
619            if (this.api instanceof SharedLinkAPIConnection) {
620                SharedLinkAPIConnection sharedItemAPI = (SharedLinkAPIConnection) this.api;
621                String sharedLink = sharedItemAPI.getSharedLink();
622                String boxAPIValue = "shared_link=" + sharedLink;
623                String sharedLinkPassword = sharedItemAPI.getSharedLinkPassword();
624                if (sharedLinkPassword != null) {
625                    boxAPIValue += "&shared_link_password=" + sharedLinkPassword;
626                }
627                connection.addRequestProperty("BoxApi", boxAPIValue);
628            }
629        }
630
631        this.requestProperties = connection.getRequestProperties();
632
633        int responseCode;
634        try {
635            this.writeBody(connection, listener);
636
637            // Ensure that we're connected in case writeBody() didn't write anything.
638            try {
639                connection.connect();
640            } catch (IOException e) {
641                throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
642            }
643
644            this.logRequest(connection);
645
646            // We need to manually handle redirects by creating a new HttpURLConnection so that connection pooling
647            // happens correctly. There seems to be a bug in Oracle's Java implementation where automatically handled
648            // redirects will not keep the connection alive.
649            try {
650                responseCode = connection.getResponseCode();
651            } catch (IOException e) {
652                throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
653            }
654        } finally {
655            if (this.api != null && this.shouldAuthenticate) {
656                this.api.unlockAccessToken();
657            }
658        }
659
660        if (isResponseRedirect(responseCode)) {
661            return this.handleRedirect(connection, listener);
662        }
663
664        String contentType = connection.getContentType();
665        BoxAPIResponse response;
666        if (contentType == null) {
667            response = new BoxAPIResponse(connection);
668        } else if (contentType.contains("application/json")) {
669            response = new BoxJSONResponse(connection);
670        } else {
671            response = new BoxAPIResponse(connection);
672        }
673
674        return response;
675    }
676
677    private BoxAPIResponse handleRedirect(HttpURLConnection connection, ProgressListener listener) {
678        if (this.numRedirects >= MAX_REDIRECTS) {
679            throw new BoxAPIException("The Box API responded with too many redirects.");
680        }
681        this.numRedirects++;
682
683        // Even though the redirect response won't have a body, we need to read the InputStream so that Java will put
684        // the connection back in the connection pool.
685        try {
686            InputStream stream = connection.getInputStream();
687            byte[] buffer = new byte[8192];
688            int n = stream.read(buffer);
689            while (n != -1) {
690                n = stream.read(buffer);
691            }
692            stream.close();
693        } catch (IOException e) {
694            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
695        }
696
697        String redirect = connection.getHeaderField("Location");
698        try {
699            this.url = new URL(redirect);
700        } catch (MalformedURLException e) {
701            throw new BoxAPIException("The Box API responded with an invalid redirect.", e);
702        }
703
704        if (this.followRedirects) {
705            return this.trySend(listener);
706        } else {
707            BoxRedirectResponse redirectResponse = new BoxRedirectResponse();
708            redirectResponse.setRedirectURL(this.url);
709            return redirectResponse;
710        }
711    }
712
713    private void logRequest(HttpURLConnection connection) {
714        if (LOGGER.isLoggable(Level.FINE)) {
715            LOGGER.log(Level.FINE, this.toString());
716        }
717    }
718
719    private HttpURLConnection createConnection() {
720        HttpURLConnection connection = null;
721
722        try {
723            if (this.api == null || this.api.getProxy() == null) {
724                connection = (HttpURLConnection) this.url.openConnection();
725            } else {
726                connection = (HttpURLConnection) this.url.openConnection(this.api.getProxy());
727            }
728        } catch (IOException e) {
729            throw new BoxAPIException("Couldn't connect to the Box API due to a network error.", e);
730        }
731
732        try {
733            connection.setRequestMethod(this.method);
734        } catch (ProtocolException e) {
735            throw new BoxAPIException("Couldn't connect to the Box API because the request's method was invalid.", e);
736        }
737
738        connection.setConnectTimeout(this.connectTimeout);
739        connection.setReadTimeout(this.readTimeout);
740
741        // Don't allow HttpURLConnection to automatically redirect because it messes up the connection pool. See the
742        // trySend(ProgressListener) method for how we handle redirects.
743        connection.setInstanceFollowRedirects(false);
744
745        for (RequestHeader header : this.headers) {
746            connection.addRequestProperty(header.getKey(), header.getValue());
747        }
748
749        return connection;
750    }
751
752    void shouldAuthenticate(boolean shouldAuthenticate) {
753        this.shouldAuthenticate = shouldAuthenticate;
754    }
755
756    /**
757     *
758     * @param  responseCode HTTP error code of the response
759     * @return true if the response is one that should be retried, otherwise false
760     */
761    public static boolean isResponseRetryable(int responseCode) {
762        return (responseCode >= 500 || responseCode == 429);
763    }
764    private static boolean isResponseRedirect(int responseCode) {
765        return (responseCode == 301 || responseCode == 302);
766    }
767
768    /**
769     * Class for mapping a request header and value.
770     */
771    public final class RequestHeader {
772        private final String key;
773        private final String value;
774
775        /**
776         * Construct a request header from header key and value.
777         * @param key header name
778         * @param value header value
779         */
780        public RequestHeader(String key, String value) {
781            this.key = key;
782            this.value = value;
783        }
784
785        /**
786         * Get header key.
787         * @return http header name
788         */
789        public String getKey() {
790            return this.key;
791        }
792
793        /**
794         * Get header value.
795         * @return http header value
796         */
797        public String getValue() {
798            return this.value;
799        }
800    }
801}