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