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