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