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