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