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