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