001package com.box.sdk;
002
003import java.net.MalformedURLException;
004import java.net.Proxy;
005import java.net.URI;
006import java.net.URL;
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.concurrent.locks.ReadWriteLock;
012import java.util.concurrent.locks.ReentrantReadWriteLock;
013import java.util.regex.Pattern;
014
015import com.eclipsesource.json.JsonObject;
016
017/**
018 * Represents an authenticated connection to the Box API.
019 *
020 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
021 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
022 * BoxAPIConnection may be created to support multi-user login.</p>
023 */
024public class BoxAPIConnection {
025    /**
026     * The default total maximum number of times an API request will be tried when error responses
027     * are received.
028     * @deprecated DEFAULT_MAX_RETRIES is preferred because it more clearly sets the number
029     * of times a request should be retried after an error response is received.
030     */
031    @Deprecated
032    public static final int DEFAULT_MAX_ATTEMPTS = 5;
033
034    /**
035     * The default maximum number of times an API request will be retried after an error response
036     * is received.
037     */
038    public static final int DEFAULT_MAX_RETRIES = 5;
039
040    private static final String AUTHORIZATION_URL = "https://account.box.com/api/oauth2/authorize";
041    private static final String TOKEN_URL_STRING = "https://api.box.com/oauth2/token";
042    private static final String REVOKE_URL_STRING = "https://api.box.com/oauth2/revoke";
043    private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/";
044    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/";
045
046    private static final String AS_USER_HEADER = "As-User";
047    private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications";
048
049    private static final String JAVA_VERSION = System.getProperty("java.version");
050    private static final String SDK_VERSION = "2.49.0";
051
052    /**
053     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
054     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
055     */
056    private static final long REFRESH_EPSILON = 60000;
057
058    private final String clientID;
059    private final String clientSecret;
060    private final ReadWriteLock refreshLock;
061
062    // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in
063    // the double-checked lock in getAccessToken(), they must be atomic.
064    private volatile long lastRefresh;
065    private volatile long expires;
066
067    private Proxy proxy;
068    private String proxyUsername;
069    private String proxyPassword;
070
071    private String userAgent;
072    private String accessToken;
073    private String refreshToken;
074    private String tokenURL;
075    private String revokeURL;
076    private String baseURL;
077    private String baseUploadURL;
078    private boolean autoRefresh;
079    private int maxRetryAttempts;
080    private int connectTimeout;
081    private int readTimeout;
082    private List<BoxAPIConnectionListener> listeners;
083    private RequestInterceptor interceptor;
084    private Map<String, String> customHeaders;
085
086    /**
087     * Used to categorize the types of resource links.
088     */
089    protected enum ResourceLinkType {
090        /**
091         * Catch-all default for resource links that are unknown.
092         */
093        Unknown,
094
095        /**
096         * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id.
097         */
098        APIEndpoint,
099
100        /**
101         * Resource URLs that point to a resource that has been shared
102         * such as https://example.box.com/s/qwertyuiop1234567890asdfghjk
103         * or https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk.
104         */
105        SharedLink
106    }
107
108    /**
109     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
110     * @param  accessToken a developer or access token to use for authenticating with the API.
111     */
112    public BoxAPIConnection(String accessToken) {
113        this(null, null, accessToken, null);
114    }
115
116    /**
117     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
118     * @param  clientID     the client ID to use when refreshing the access token.
119     * @param  clientSecret the client secret to use when refreshing the access token.
120     * @param  accessToken  an initial access token to use for authenticating with the API.
121     * @param  refreshToken an initial refresh token to use when refreshing the access token.
122     */
123    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
124        this.clientID = clientID;
125        this.clientSecret = clientSecret;
126        this.accessToken = accessToken;
127        this.refreshToken = refreshToken;
128        this.tokenURL = TOKEN_URL_STRING;
129        this.revokeURL = REVOKE_URL_STRING;
130        this.baseURL = DEFAULT_BASE_URL;
131        this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL;
132        this.autoRefresh = true;
133        this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts();
134        this.connectTimeout = BoxGlobalSettings.getConnectTimeout();
135        this.readTimeout = BoxGlobalSettings.getReadTimeout();
136        this.refreshLock = new ReentrantReadWriteLock();
137        this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")";
138        this.listeners = new ArrayList<BoxAPIConnectionListener>();
139        this.customHeaders = new HashMap<String, String>();
140    }
141
142    /**
143     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
144     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
145     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
146     * @param  authCode     an auth code obtained from the first half of the OAuth process.
147     */
148    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
149        this(clientID, clientSecret, null, null);
150        this.authenticate(authCode);
151    }
152
153    /**
154     * Constructs a new BoxAPIConnection.
155     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
156     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
157     */
158    public BoxAPIConnection(String clientID, String clientSecret) {
159        this(clientID, clientSecret, null, null);
160    }
161
162    /**
163     * Constructs a new BoxAPIConnection levaraging BoxConfig.
164     * @param  boxConfig     BoxConfig file, which should have clientId and clientSecret
165     */
166    public BoxAPIConnection(BoxConfig boxConfig) {
167        this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null);
168    }
169
170    /**
171     * Restores a BoxAPIConnection from a saved state.
172     *
173     * @see    #save
174     * @param  clientID     the client ID to use with the connection.
175     * @param  clientSecret the client secret to use with the connection.
176     * @param  state        the saved state that was created with {@link #save}.
177     * @return              a restored API connection.
178     */
179    public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
180        BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
181        api.restore(state);
182        return api;
183    }
184
185    /**
186     * Return the authorization URL which is used to perform the authorization_code based OAuth2 flow.
187     * @param clientID the client ID to use with the connection.
188     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
189     * @param state the text string that you choose.
190     *              Box sends the same string to your redirect URL when authentication is complete.
191     * @param scopes this optional parameter identifies the Box scopes available
192     *               to the application once it's authenticated.
193     * @return the authorization URL
194     */
195    public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) {
196        URLTemplate template = new URLTemplate(AUTHORIZATION_URL);
197        QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID)
198                .appendParam("response_type", "code")
199                .appendParam("redirect_uri", redirectUri.toString())
200                .appendParam("state", state);
201
202        if (scopes != null && !scopes.isEmpty()) {
203            StringBuilder builder = new StringBuilder();
204            int size = scopes.size() - 1;
205            int i = 0;
206            while (i < size) {
207                builder.append(scopes.get(i));
208                builder.append(" ");
209                i++;
210            }
211            builder.append(scopes.get(i));
212
213            queryBuilder.appendParam("scope", builder.toString());
214        }
215
216        return template.buildWithQuery("", queryBuilder.toString());
217    }
218
219    /**
220     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
221     * from the first half of OAuth.
222     * @param authCode the auth code obtained from the first half of the OAuth process.
223     */
224    public void authenticate(String authCode) {
225        URL url = null;
226        try {
227            url = new URL(this.tokenURL);
228        } catch (MalformedURLException e) {
229            assert false : "An invalid token URL indicates a bug in the SDK.";
230            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
231        }
232
233        String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
234            authCode, this.clientID, this.clientSecret);
235
236        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
237        request.shouldAuthenticate(false);
238        request.setBody(urlParameters);
239
240        BoxJSONResponse response = (BoxJSONResponse) request.send();
241        String json = response.getJSON();
242
243        JsonObject jsonObject = JsonObject.readFrom(json);
244        this.accessToken = jsonObject.get("access_token").asString();
245        this.refreshToken = jsonObject.get("refresh_token").asString();
246        this.lastRefresh = System.currentTimeMillis();
247        this.expires = jsonObject.get("expires_in").asLong() * 1000;
248    }
249
250    /**
251     * Gets the client ID.
252     * @return the client ID.
253     */
254    public String getClientID() {
255        return this.clientID;
256    }
257
258    /**
259     * Gets the client secret.
260     * @return the client secret.
261     */
262    public String getClientSecret() {
263        return this.clientSecret;
264    }
265
266    /**
267     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
268     * @param milliseconds the number of milliseconds for which the access token is valid.
269     */
270    public void setExpires(long milliseconds) {
271        this.expires = milliseconds;
272    }
273
274    /**
275     * Gets the amount of time for which this connection's access token is valid.
276     * @return the amount of time in milliseconds.
277     */
278    public long getExpires() {
279        return this.expires;
280    }
281
282    /**
283     * Gets the token URL that's used to request access tokens.  The default value is
284     * "https://www.box.com/api/oauth2/token".
285     * @return the token URL.
286     */
287    public String getTokenURL() {
288        return this.tokenURL;
289    }
290
291    /**
292     * Sets the token URL that's used to request access tokens.  For example, the default token URL is
293     * "https://www.box.com/api/oauth2/token".
294     * @param tokenURL the token URL.
295     */
296    public void setTokenURL(String tokenURL) {
297        this.tokenURL = tokenURL;
298    }
299
300    /**
301     * Set the URL used for token revocation.
302     * @param url The url to use.
303     */
304    public void setRevokeURL(String url) {
305        this.revokeURL = url;
306    }
307
308    /**
309     * Returns the URL used for token revocation.
310     * @return The url used for token revocation.
311     */
312    public String getRevokeURL() {
313        return this.revokeURL;
314    }
315
316    /**
317     * Gets the base URL that's used when sending requests to the Box API. The default value is
318     * "https://api.box.com/2.0/".
319     * @return the base URL.
320     */
321    public String getBaseURL() {
322        return this.baseURL;
323    }
324
325    /**
326     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
327     * "https://api.box.com/2.0/".
328     * @param baseURL a base URL
329     */
330    public void setBaseURL(String baseURL) {
331        this.baseURL = baseURL;
332    }
333
334    /**
335     * Gets the base upload URL that's used when performing file uploads to Box.
336     * @return the base upload URL.
337     */
338    public String getBaseUploadURL() {
339        return this.baseUploadURL;
340    }
341
342    /**
343     * Sets the base upload URL to be used when performing file uploads to Box.
344     * @param baseUploadURL a base upload URL.
345     */
346    public void setBaseUploadURL(String baseUploadURL) {
347        this.baseUploadURL = baseUploadURL;
348    }
349
350    /**
351     * Gets the user agent that's used when sending requests to the Box API.
352     * @return the user agent.
353     */
354    public String getUserAgent() {
355        return this.userAgent;
356    }
357
358    /**
359     * Sets the user agent to be used when sending requests to the Box API.
360     * @param userAgent the user agent.
361     */
362    public void setUserAgent(String userAgent) {
363        this.userAgent = userAgent;
364    }
365
366    /**
367     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
368     * access token if it has expired since the last call to <code>getAccessToken()</code>.
369     * @return a valid access token that can be used to authenticate an API request.
370     */
371    public String getAccessToken() {
372        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
373            this.refreshLock.writeLock().lock();
374            try {
375                if (this.needsRefresh()) {
376                    this.refresh();
377                }
378            } finally {
379                this.refreshLock.writeLock().unlock();
380            }
381        }
382
383        return this.accessToken;
384    }
385
386    /**
387     * Sets the access token to use when authenticating API requests.
388     * @param accessToken a valid access token to use when authenticating API requests.
389     */
390    public void setAccessToken(String accessToken) {
391        this.accessToken = accessToken;
392    }
393
394    /**
395     * Gets the refresh lock to be used when refreshing an access token.
396     * @return the refresh lock.
397     */
398    protected ReadWriteLock getRefreshLock() {
399        return this.refreshLock;
400    }
401    /**
402     * Gets a refresh token that can be used to refresh an access token.
403     * @return a valid refresh token.
404     */
405    public String getRefreshToken() {
406        return this.refreshToken;
407    }
408
409    /**
410     * Sets the refresh token to use when refreshing an access token.
411     * @param refreshToken a valid refresh token.
412     */
413    public void setRefreshToken(String refreshToken) {
414        this.refreshToken = refreshToken;
415    }
416
417    /**
418     * Gets the last time that the access token was refreshed.
419     *
420     * @return the last refresh time in milliseconds.
421     */
422    public long getLastRefresh() {
423        return this.lastRefresh;
424    }
425
426    /**
427     * Sets the last time that the access token was refreshed.
428     *
429     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
430     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
431     *
432     * @param lastRefresh the new last refresh time in milliseconds.
433     */
434    public void setLastRefresh(long lastRefresh) {
435        this.lastRefresh = lastRefresh;
436    }
437
438    /**
439     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
440     * @param autoRefresh true to enable auto token refresh; otherwise false.
441     */
442    public void setAutoRefresh(boolean autoRefresh) {
443        this.autoRefresh = autoRefresh;
444    }
445
446    /**
447     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
448     * @return true if auto token refresh is enabled; otherwise false.
449     */
450    public boolean getAutoRefresh() {
451        return this.autoRefresh;
452    }
453
454    /**
455     * Sets the total maximum number of times an API request will be tried when error responses
456     * are received.
457     * @return the maximum number of request attempts.
458     * @deprecated getMaxRetryAttempts is preferred because it more clearly gets the number
459     * of times a request should be retried after an error response is received.
460     */
461    @Deprecated
462    public int getMaxRequestAttempts() {
463        return this.maxRetryAttempts + 1;
464    }
465
466    /**
467     * Sets the total maximum number of times an API request will be tried when error responses
468     * are received.
469     * @param attempts the maximum number of request attempts.
470     * @deprecated setMaxRetryAttempts is preferred because it more clearly sets the number
471     * of times a request should be retried after an error response is received.
472     */
473    @Deprecated
474    public void setMaxRequestAttempts(int attempts) {
475        this.maxRetryAttempts = attempts - 1;
476    }
477
478    /**
479     * Gets the maximum number of times an API request will be retried after an error response
480     * is received.
481     * @return the maximum number of request attempts.
482     */
483    public int getMaxRetryAttempts() {
484        return this.maxRetryAttempts;
485    }
486
487    /**
488     * Sets the maximum number of times an API request will be retried after an error response
489     * is received.
490     * @param attempts the maximum number of request attempts.
491     */
492    public void setMaxRetryAttempts(int attempts) {
493        this.maxRetryAttempts = attempts;
494    }
495
496    /**
497     * Gets the connect timeout for this connection in milliseconds.
498     * @return the number of milliseconds to connect before timing out.
499     */
500    public int getConnectTimeout() {
501        return this.connectTimeout;
502    }
503
504    /**
505     * Sets the connect timeout for this connection.
506     * @param connectTimeout The number of milliseconds to wait for the connection to be established.
507     */
508    public void setConnectTimeout(int connectTimeout) {
509        this.connectTimeout = connectTimeout;
510    }
511
512    /**
513     * Gets the read timeout for this connection in milliseconds.
514     * @return the number of milliseconds to wait for bytes to be read before timing out.
515     */
516    public int getReadTimeout() {
517        return this.readTimeout;
518    }
519
520    /**
521     * Sets the read timeout for this connection.
522     * @param readTimeout The number of milliseconds to wait for bytes to be read.
523     */
524    public void setReadTimeout(int readTimeout) {
525        this.readTimeout = readTimeout;
526    }
527
528    /**
529     * Gets the proxy value to use for API calls to Box.
530     * @return the current proxy.
531     */
532    public Proxy getProxy() {
533        return this.proxy;
534    }
535
536    /**
537     * Sets the proxy to use for API calls to Box.
538     * @param proxy the proxy to use for API calls to Box.
539     */
540    public void setProxy(Proxy proxy) {
541        this.proxy = proxy;
542    }
543
544    /**
545     * Gets the username to use for a proxy that requires basic auth.
546     * @return the username to use for a proxy that requires basic auth.
547     */
548    public String getProxyUsername() {
549        return this.proxyUsername;
550    }
551
552    /**
553     * Sets the username to use for a proxy that requires basic auth.
554     * @param proxyUsername the username to use for a proxy that requires basic auth.
555     */
556    public void setProxyUsername(String proxyUsername) {
557        this.proxyUsername = proxyUsername;
558    }
559
560    /**
561     * Gets the password to use for a proxy that requires basic auth.
562     * @return the password to use for a proxy that requires basic auth.
563     */
564    public String getProxyPassword() {
565        return this.proxyPassword;
566    }
567
568    /**
569     * Sets the password to use for a proxy that requires basic auth.
570     * @param proxyPassword the password to use for a proxy that requires basic auth.
571     */
572    public void setProxyPassword(String proxyPassword) {
573        this.proxyPassword = proxyPassword;
574    }
575
576    /**
577     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
578     * token was never set.
579     * @return true if the access token can be refreshed; otherwise false.
580     */
581    public boolean canRefresh() {
582        return this.refreshToken != null;
583    }
584
585    /**
586     * Determines if this connection's access token has expired and needs to be refreshed.
587     * @return true if the access token needs to be refreshed; otherwise false.
588     */
589    public boolean needsRefresh() {
590        boolean needsRefresh;
591
592        this.refreshLock.readLock().lock();
593        long now = System.currentTimeMillis();
594        long tokenDuration = (now - this.lastRefresh);
595        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
596        this.refreshLock.readLock().unlock();
597
598        return needsRefresh;
599    }
600
601    /**
602     * Refresh's this connection's access token using its refresh token.
603     * @throws IllegalStateException if this connection's access token cannot be refreshed.
604     */
605    public void refresh() {
606        this.refreshLock.writeLock().lock();
607
608        if (!this.canRefresh()) {
609            this.refreshLock.writeLock().unlock();
610            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
611                + "refresh token.");
612        }
613
614        URL url = null;
615        try {
616            url = new URL(this.tokenURL);
617        } catch (MalformedURLException e) {
618            this.refreshLock.writeLock().unlock();
619            assert false : "An invalid refresh URL indicates a bug in the SDK.";
620            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
621        }
622
623        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
624            this.refreshToken, this.clientID, this.clientSecret);
625
626        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
627        request.shouldAuthenticate(false);
628        request.setBody(urlParameters);
629
630        String json;
631        try {
632            BoxJSONResponse response = (BoxJSONResponse) request.send();
633            json = response.getJSON();
634        } catch (BoxAPIException e) {
635            this.notifyError(e);
636            this.refreshLock.writeLock().unlock();
637            throw e;
638        }
639
640        JsonObject jsonObject = JsonObject.readFrom(json);
641        this.accessToken = jsonObject.get("access_token").asString();
642        this.refreshToken = jsonObject.get("refresh_token").asString();
643        this.lastRefresh = System.currentTimeMillis();
644        this.expires = jsonObject.get("expires_in").asLong() * 1000;
645
646        this.notifyRefresh();
647
648        this.refreshLock.writeLock().unlock();
649    }
650
651    /**
652     * Restores a saved connection state into this BoxAPIConnection.
653     *
654     * @see    #save
655     * @param  state the saved state that was created with {@link #save}.
656     */
657    public void restore(String state) {
658        JsonObject json = JsonObject.readFrom(state);
659        String accessToken = json.get("accessToken").asString();
660        String refreshToken = json.get("refreshToken").asString();
661        long lastRefresh = json.get("lastRefresh").asLong();
662        long expires = json.get("expires").asLong();
663        String userAgent = json.get("userAgent").asString();
664        String tokenURL = json.get("tokenURL").asString();
665        String baseURL = json.get("baseURL").asString();
666        String baseUploadURL = json.get("baseUploadURL").asString();
667        boolean autoRefresh = json.get("autoRefresh").asBoolean();
668
669        // Try to read deprecated value
670        int maxRequestAttempts = -1;
671        if (json.names().contains("maxRequestAttempts")) {
672            maxRequestAttempts = json.get("maxRequestAttempts").asInt();
673        }
674
675        int maxRetryAttempts = -1;
676        if (json.names().contains("maxRetryAttempts")) {
677            maxRetryAttempts = json.get("maxRetryAttempts").asInt();
678        }
679
680        this.accessToken = accessToken;
681        this.refreshToken = refreshToken;
682        this.lastRefresh = lastRefresh;
683        this.expires = expires;
684        this.userAgent = userAgent;
685        this.tokenURL = tokenURL;
686        this.baseURL = baseURL;
687        this.baseUploadURL = baseUploadURL;
688        this.autoRefresh = autoRefresh;
689
690        // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts"
691        if (maxRequestAttempts > -1) {
692            this.maxRetryAttempts = maxRequestAttempts - 1;
693        } else {
694            this.maxRetryAttempts = maxRetryAttempts;
695        }
696
697    }
698
699    /**
700     * Notifies a refresh event to all the listeners.
701     */
702    protected void notifyRefresh() {
703        for (BoxAPIConnectionListener listener : this.listeners) {
704            listener.onRefresh(this);
705        }
706    }
707
708    /**
709     * Notifies an error event to all the listeners.
710     * @param error A BoxAPIException instance.
711     */
712    protected void notifyError(BoxAPIException error) {
713        for (BoxAPIConnectionListener listener : this.listeners) {
714            listener.onError(this, error);
715        }
716    }
717
718    /**
719     * Add a listener to listen to Box API connection events.
720     * @param listener a listener to listen to Box API connection.
721     */
722    public void addListener(BoxAPIConnectionListener listener) {
723        this.listeners.add(listener);
724    }
725
726    /**
727     * Remove a listener listening to Box API connection events.
728     * @param listener the listener to remove.
729     */
730    public void removeListener(BoxAPIConnectionListener listener) {
731        this.listeners.remove(listener);
732    }
733
734    /**
735     * Gets the RequestInterceptor associated with this API connection.
736     * @return the RequestInterceptor associated with this API connection.
737     */
738    public RequestInterceptor getRequestInterceptor() {
739        return this.interceptor;
740    }
741
742    /**
743     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
744     * @param interceptor the RequestInterceptor.
745     */
746    public void setRequestInterceptor(RequestInterceptor interceptor) {
747        this.interceptor = interceptor;
748    }
749
750    /**
751     * Get a lower-scoped token restricted to a resource for the list of scopes that are passed.
752     * @param scopes the list of scopes to which the new token should be restricted for
753     * @param resource the resource for which the new token has to be obtained
754     * @return scopedToken which has access token and other details
755     * @throws BoxAPIException if resource is not a valid Box API endpoint or shared link
756     */
757    public ScopedToken getLowerScopedToken(List<String> scopes, String resource) {
758        assert (scopes != null);
759        assert (scopes.size() > 0);
760        URL url = null;
761        try {
762            url = new URL(this.getTokenURL());
763        } catch (MalformedURLException e) {
764            assert false : "An invalid refresh URL indicates a bug in the SDK.";
765            throw new BoxAPIException("An invalid refresh URL indicates a bug in the SDK.", e);
766        }
767
768        StringBuilder spaceSeparatedScopes = this.buildScopesForTokenDownscoping(scopes);
769
770        String urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
771                        + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
772                        + "&scope=%s",
773                this.getAccessToken(), spaceSeparatedScopes);
774
775        if (resource != null) {
776
777            ResourceLinkType resourceType = this.determineResourceLinkType(resource);
778
779            if (resourceType == ResourceLinkType.APIEndpoint) {
780                urlParameters = String.format(urlParameters + "&resource=%s", resource);
781            } else if (resourceType == ResourceLinkType.SharedLink) {
782                urlParameters = String.format(urlParameters + "&box_shared_link=%s", resource);
783            } else if (resourceType == ResourceLinkType.Unknown) {
784                String argExceptionMessage = String.format("Unable to determine resource type: %s", resource);
785                BoxAPIException e = new BoxAPIException(argExceptionMessage);
786                this.notifyError(e);
787                throw e;
788            } else {
789                String argExceptionMessage = String.format("Unhandled resource type: %s", resource);
790                BoxAPIException e = new BoxAPIException(argExceptionMessage);
791                this.notifyError(e);
792                throw e;
793            }
794        }
795
796        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
797        request.shouldAuthenticate(false);
798        request.setBody(urlParameters);
799
800        String jsonResponse;
801        try {
802            BoxJSONResponse response = (BoxJSONResponse) request.send();
803            jsonResponse = response.getJSON();
804        } catch (BoxAPIException e) {
805            this.notifyError(e);
806            throw e;
807        }
808
809        JsonObject jsonObject = JsonObject.readFrom(jsonResponse);
810        ScopedToken token = new ScopedToken(jsonObject);
811        token.setObtainedAt(System.currentTimeMillis());
812        token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000);
813        return token;
814    }
815
816    /**
817     * Convert List<String> to space-delimited String.
818     * Needed for versions prior to Java 8, which don't have String.join(delimiter, list)
819     * @param scopes the list of scopes to read from
820     * @return space-delimited String of scopes
821     */
822    private StringBuilder buildScopesForTokenDownscoping(List<String> scopes) {
823        StringBuilder spaceSeparatedScopes = new StringBuilder();
824        for (int i = 0; i < scopes.size(); i++) {
825            spaceSeparatedScopes.append(scopes.get(i));
826            if (i < scopes.size() - 1) {
827                spaceSeparatedScopes.append(" ");
828            }
829        }
830
831        return spaceSeparatedScopes;
832    }
833
834    /**
835     * Determines the type of resource, given a link to a Box resource.
836     * @param resourceLink the resource URL to check
837     * @return ResourceLinkType that categorizes the provided resourceLink
838     */
839    protected ResourceLinkType determineResourceLinkType(String resourceLink) {
840
841        ResourceLinkType resourceType = ResourceLinkType.Unknown;
842
843        try {
844            URL validUrl = new URL(resourceLink);
845            String validURLStr = validUrl.toString();
846            final String apiEndpointPattern = "https://api.box.com/2.0/files/\\d+";
847            final String sharedLinkPattern = "(.*box.com/s/.*|.*box.com.*s=.*)";
848
849            if (Pattern.matches(apiEndpointPattern, validURLStr)) {
850                resourceType = ResourceLinkType.APIEndpoint;
851            } else if (Pattern.matches(sharedLinkPattern, validURLStr)) {
852                resourceType = ResourceLinkType.SharedLink;
853            }
854        } catch (MalformedURLException e) {
855             //Swallow exception and return default ResourceLinkType set at top of function
856        }
857
858        return resourceType;
859    }
860
861    /**
862     * Revokes the tokens associated with this API connection.  This results in the connection no
863     * longer being able to make API calls until a fresh authorization is made by calling authenticate()
864     */
865    public void revokeToken() {
866
867        URL url = null;
868        try {
869            url = new URL(this.revokeURL);
870        } catch (MalformedURLException e) {
871            assert false : "An invalid refresh URL indicates a bug in the SDK.";
872            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
873        }
874
875        String urlParameters = String.format("token=%s&client_id=%s&client_secret=%s",
876                this.accessToken, this.clientID, this.clientSecret);
877
878        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
879        request.shouldAuthenticate(false);
880        request.setBody(urlParameters);
881
882        try {
883            request.send();
884        } catch (BoxAPIException e) {
885            throw e;
886        }
887    }
888
889    /**
890     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
891     *
892     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
893     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
894     * have to manually configure it again after restoring the connection.</p>
895     *
896     * @see    #restore
897     * @return the state of this connection.
898     */
899    public String save() {
900        JsonObject state = new JsonObject()
901            .add("accessToken", this.accessToken)
902            .add("refreshToken", this.refreshToken)
903            .add("lastRefresh", this.lastRefresh)
904            .add("expires", this.expires)
905            .add("userAgent", this.userAgent)
906            .add("tokenURL", this.tokenURL)
907            .add("baseURL", this.baseURL)
908            .add("baseUploadURL", this.baseUploadURL)
909            .add("autoRefresh", this.autoRefresh)
910            .add("maxRetryAttempts", this.maxRetryAttempts);
911        return state.toString();
912    }
913
914    String lockAccessToken() {
915        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
916            this.refreshLock.writeLock().lock();
917            try {
918                if (this.needsRefresh()) {
919                    this.refresh();
920                }
921                this.refreshLock.readLock().lock();
922            } finally {
923                this.refreshLock.writeLock().unlock();
924            }
925        } else {
926            this.refreshLock.readLock().lock();
927        }
928
929        return this.accessToken;
930    }
931
932    void unlockAccessToken() {
933        this.refreshLock.readLock().unlock();
934    }
935
936    /**
937     * Get the value for the X-Box-UA header.
938     * @return the header value.
939     */
940    String getBoxUAHeader() {
941
942        return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION;
943    }
944
945    /**
946     * Sets a custom header to be sent on all requests through this API connection.
947     * @param header the header name.
948     * @param value the header value.
949     */
950    public void setCustomHeader(String header, String value) {
951        this.customHeaders.put(header, value);
952    }
953
954    /**
955     * Removes a custom header, so it will no longer be sent on requests through this API connection.
956     * @param header the header name.
957     */
958    public void removeCustomHeader(String header) {
959        this.customHeaders.remove(header);
960    }
961
962    /**
963     * Suppresses email notifications from API actions.  This is typically used by security or admin applications
964     * to prevent spamming end users when doing automated processing on their content.
965     */
966    public void suppressNotifications() {
967        this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off");
968    }
969
970    /**
971     * Re-enable email notifications from API actions if they have been suppressed.
972     * @see #suppressNotifications
973     */
974    public void enableNotifications() {
975        this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER);
976    }
977
978    /**
979     * Set this API connection to make API calls on behalf of another users, impersonating them.  This
980     * functionality can only be used by admins and service accounts.
981     * @param userID the ID of the user to act as.
982     */
983    public void asUser(String userID) {
984        this.setCustomHeader(AS_USER_HEADER, userID);
985    }
986
987    /**
988     * Sets this API connection to make API calls on behalf of the user with whom the access token is associated.
989     * This undoes any previous calls to asUser().
990     * @see #asUser
991     */
992    public void asSelf() {
993        this.removeCustomHeader(AS_USER_HEADER);
994    }
995
996    Map<String, String> getHeaders() {
997        return this.customHeaders;
998    }
999}