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