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