001package com.box.sdk;
002
003import java.net.MalformedURLException;
004import java.net.Proxy;
005import java.net.URL;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.concurrent.locks.ReadWriteLock;
009import java.util.concurrent.locks.ReentrantReadWriteLock;
010
011import com.eclipsesource.json.JsonObject;
012
013/**
014 * Represents an authenticated connection to the Box API.
015 *
016 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
017 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
018 * BoxAPIConnection may be created to support multi-user login.</p>
019 */
020public class BoxAPIConnection {
021    /**
022     * The default maximum number of times an API request will be tried when an error occurs.
023     */
024    public static final int DEFAULT_MAX_ATTEMPTS = 3;
025
026    private static final String TOKEN_URL_STRING = "https://api.box.com/oauth2/token";
027    private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/";
028    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/";
029
030    /**
031     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
032     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
033     */
034    private static final long REFRESH_EPSILON = 60000;
035
036    private final String clientID;
037    private final String clientSecret;
038    private final ReadWriteLock refreshLock;
039
040    // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in
041    // the double-checked lock in getAccessToken(), they must be atomic.
042    private volatile long lastRefresh;
043    private volatile long expires;
044
045    private Proxy proxy;
046    private String proxyUsername;
047    private String proxyPassword;
048
049    private String userAgent;
050    private String accessToken;
051    private String refreshToken;
052    private String tokenURL;
053    private String baseURL;
054    private String baseUploadURL;
055    private boolean autoRefresh;
056    private int maxRequestAttempts;
057    private List<BoxAPIConnectionListener> listeners;
058    private RequestInterceptor interceptor;
059
060    /**
061     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
062     * @param  accessToken a developer or access token to use for authenticating with the API.
063     */
064    public BoxAPIConnection(String accessToken) {
065        this(null, null, accessToken, null);
066    }
067
068    /**
069     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
070     * @param  clientID     the client ID to use when refreshing the access token.
071     * @param  clientSecret the client secret to use when refreshing the access token.
072     * @param  accessToken  an initial access token to use for authenticating with the API.
073     * @param  refreshToken an initial refresh token to use when refreshing the access token.
074     */
075    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
076        this.clientID = clientID;
077        this.clientSecret = clientSecret;
078        this.accessToken = accessToken;
079        this.refreshToken = refreshToken;
080        this.tokenURL = TOKEN_URL_STRING;
081        this.baseURL = DEFAULT_BASE_URL;
082        this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL;
083        this.autoRefresh = true;
084        this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS;
085        this.refreshLock = new ReentrantReadWriteLock();
086        this.userAgent = "Box Java SDK v2.0.0";
087        this.listeners = new ArrayList<BoxAPIConnectionListener>();
088    }
089
090    /**
091     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
092     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
093     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
094     * @param  authCode     an auth code obtained from the first half of the OAuth process.
095     */
096    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
097        this(clientID, clientSecret, null, null);
098        this.authenticate(authCode);
099    }
100
101    /**
102     * Constructs a new BoxAPIConnection.
103     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
104     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
105     */
106    public BoxAPIConnection(String clientID, String clientSecret) {
107        this(clientID, clientSecret, null, null);
108    }
109
110    /**
111     * Restores a BoxAPIConnection from a saved state.
112     *
113     * @see    #save
114     * @param  clientID     the client ID to use with the connection.
115     * @param  clientSecret the client secret to use with the connection.
116     * @param  state        the saved state that was created with {@link #save}.
117     * @return              a restored API connection.
118     */
119    public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
120        BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
121        api.restore(state);
122        return api;
123    }
124
125    /**
126     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
127     * from the first half of OAuth.
128     * @param authCode the auth code obtained from the first half of the OAuth process.
129     */
130    public void authenticate(String authCode) {
131        URL url = null;
132        try {
133            url = new URL(this.tokenURL);
134        } catch (MalformedURLException e) {
135            assert false : "An invalid token URL indicates a bug in the SDK.";
136            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
137        }
138
139        String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
140            authCode, this.clientID, this.clientSecret);
141
142        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
143        request.shouldAuthenticate(false);
144        request.setBody(urlParameters);
145
146        BoxJSONResponse response = (BoxJSONResponse) request.send();
147        String json = response.getJSON();
148
149        JsonObject jsonObject = JsonObject.readFrom(json);
150        this.accessToken = jsonObject.get("access_token").asString();
151        this.refreshToken = jsonObject.get("refresh_token").asString();
152        this.lastRefresh = System.currentTimeMillis();
153        this.expires = jsonObject.get("expires_in").asLong() * 1000;
154    }
155
156    /**
157     * Gets the client ID.
158     * @return the client ID.
159     */
160    public String getClientID() {
161        return this.clientID;
162    }
163
164    /**
165     * Gets the client secret.
166     * @return the client secret.
167     */
168    public String getClientSecret() {
169        return this.clientSecret;
170    }
171
172    /**
173     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
174     * @param milliseconds the number of milliseconds for which the access token is valid.
175     */
176    public void setExpires(long milliseconds) {
177        this.expires = milliseconds;
178    }
179
180    /**
181     * Gets the amount of time for which this connection's access token is valid.
182     * @return the amount of time in milliseconds.
183     */
184    public long getExpires() {
185        return this.expires;
186    }
187
188    /**
189     * Gets the token URL that's used to request access tokens.  The default value is
190     * "https://www.box.com/api/oauth2/token".
191     * @return the token URL.
192     */
193    public String getTokenURL() {
194        return this.tokenURL;
195    }
196
197    /**
198     * Sets the token URL that's used to request access tokens.  For example, the default token URL is
199     * "https://www.box.com/api/oauth2/token".
200     * @param tokenURL the token URL.
201     */
202    public void setTokenURL(String tokenURL) {
203        this.tokenURL = tokenURL;
204    }
205
206    /**
207     * Gets the base URL that's used when sending requests to the Box API. The default value is
208     * "https://api.box.com/2.0/".
209     * @return the base URL.
210     */
211    public String getBaseURL() {
212        return this.baseURL;
213    }
214
215    /**
216     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
217     * "https://api.box.com/2.0/".
218     * @param baseURL a base URL
219     */
220    public void setBaseURL(String baseURL) {
221        this.baseURL = baseURL;
222    }
223
224    /**
225     * Gets the base upload URL that's used when performing file uploads to Box.
226     * @return the base upload URL.
227     */
228    public String getBaseUploadURL() {
229        return this.baseUploadURL;
230    }
231
232    /**
233     * Sets the base upload URL to be used when performing file uploads to Box.
234     * @param baseUploadURL a base upload URL.
235     */
236    public void setBaseUploadURL(String baseUploadURL) {
237        this.baseUploadURL = baseUploadURL;
238    }
239
240    /**
241     * Gets the user agent that's used when sending requests to the Box API.
242     * @return the user agent.
243     */
244    public String getUserAgent() {
245        return this.userAgent;
246    }
247
248    /**
249     * Sets the user agent to be used when sending requests to the Box API.
250     * @param userAgent the user agent.
251     */
252    public void setUserAgent(String userAgent) {
253        this.userAgent = userAgent;
254    }
255
256    /**
257     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
258     * access token if it has expired since the last call to <code>getAccessToken()</code>.
259     * @return a valid access token that can be used to authenticate an API request.
260     */
261    public String getAccessToken() {
262        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
263            this.refreshLock.writeLock().lock();
264            try {
265                if (this.needsRefresh()) {
266                    this.refresh();
267                }
268            } finally {
269                this.refreshLock.writeLock().unlock();
270            }
271        }
272
273        return this.accessToken;
274    }
275
276    /**
277     * Sets the access token to use when authenticating API requests.
278     * @param accessToken a valid access token to use when authenticating API requests.
279     */
280    public void setAccessToken(String accessToken) {
281        this.accessToken = accessToken;
282    }
283
284    /**
285     * Gets the refresh lock to be used when refreshing an access token.
286     * @return the refresh lock.
287     */
288    protected ReadWriteLock getRefreshLock() {
289        return this.refreshLock;
290    }
291    /**
292     * Gets a refresh token that can be used to refresh an access token.
293     * @return a valid refresh token.
294     */
295    public String getRefreshToken() {
296        return this.refreshToken;
297    }
298
299    /**
300     * Sets the refresh token to use when refreshing an access token.
301     * @param refreshToken a valid refresh token.
302     */
303    public void setRefreshToken(String refreshToken) {
304        this.refreshToken = refreshToken;
305    }
306
307    /**
308     * Gets the last time that the access token was refreshed.
309     *
310     * @return the last refresh time in milliseconds.
311     */
312    public long getLastRefresh() {
313        return this.lastRefresh;
314    }
315
316    /**
317     * Sets the last time that the access token was refreshed.
318     *
319     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
320     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
321     *
322     * @param lastRefresh the new last refresh time in milliseconds.
323     */
324    public void setLastRefresh(long lastRefresh) {
325        this.lastRefresh = lastRefresh;
326    }
327
328    /**
329     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
330     * @param autoRefresh true to enable auto token refresh; otherwise false.
331     */
332    public void setAutoRefresh(boolean autoRefresh) {
333        this.autoRefresh = autoRefresh;
334    }
335
336    /**
337     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
338     * @return true if auto token refresh is enabled; otherwise false.
339     */
340    public boolean getAutoRefresh() {
341        return this.autoRefresh;
342    }
343
344    /**
345     * Gets the maximum number of times an API request will be tried when an error occurs.
346     * @return the maximum number of request attempts.
347     */
348    public int getMaxRequestAttempts() {
349        return this.maxRequestAttempts;
350    }
351
352    /**
353     * Sets the maximum number of times an API request will be tried when an error occurs.
354     * @param attempts the maximum number of request attempts.
355     */
356    public void setMaxRequestAttempts(int attempts) {
357        this.maxRequestAttempts = attempts;
358    }
359
360    /**
361     * Gets the proxy value to use for API calls to Box.
362     * @return the current proxy.
363     */
364    public Proxy getProxy() {
365        return this.proxy;
366    }
367
368    /**
369     * Sets the proxy to use for API calls to Box.
370     * @param proxy the proxy to use for API calls to Box.
371     */
372    public void setProxy(Proxy proxy) {
373        this.proxy = proxy;
374    }
375
376    /**
377     * Gets the username to use for a proxy that requires basic auth.
378     * @return the username to use for a proxy that requires basic auth.
379     */
380    public String getProxyUsername() {
381        return this.proxyUsername;
382    }
383
384    /**
385     * Sets the username to use for a proxy that requires basic auth.
386     * @param proxyUsername the username to use for a proxy that requires basic auth.
387     */
388    public void setProxyUsername(String proxyUsername) {
389        this.proxyUsername = proxyUsername;
390    }
391
392    /**
393     * Gets the password to use for a proxy that requires basic auth.
394     * @return the password to use for a proxy that requires basic auth.
395     */
396    public String getProxyPassword() {
397        return this.proxyPassword;
398    }
399
400    /**
401     * Sets the password to use for a proxy that requires basic auth.
402     * @param proxyPassword the password to use for a proxy that requires basic auth.
403     */
404    public void setProxyPassword(String proxyPassword) {
405        this.proxyPassword = proxyPassword;
406    }
407
408    /**
409     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
410     * token was never set.
411     * @return true if the access token can be refreshed; otherwise false.
412     */
413    public boolean canRefresh() {
414        return this.refreshToken != null;
415    }
416
417    /**
418     * Determines if this connection's access token has expired and needs to be refreshed.
419     * @return true if the access token needs to be refreshed; otherwise false.
420     */
421    public boolean needsRefresh() {
422        boolean needsRefresh;
423
424        this.refreshLock.readLock().lock();
425        long now = System.currentTimeMillis();
426        long tokenDuration = (now - this.lastRefresh);
427        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
428        this.refreshLock.readLock().unlock();
429
430        return needsRefresh;
431    }
432
433    /**
434     * Refresh's this connection's access token using its refresh token.
435     * @throws IllegalStateException if this connection's access token cannot be refreshed.
436     */
437    public void refresh() {
438        this.refreshLock.writeLock().lock();
439
440        if (!this.canRefresh()) {
441            this.refreshLock.writeLock().unlock();
442            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
443                + "refresh token.");
444        }
445
446        URL url = null;
447        try {
448            url = new URL(this.tokenURL);
449        } catch (MalformedURLException e) {
450            this.refreshLock.writeLock().unlock();
451            assert false : "An invalid refresh URL indicates a bug in the SDK.";
452            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
453        }
454
455        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
456            this.refreshToken, this.clientID, this.clientSecret);
457
458        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
459        request.shouldAuthenticate(false);
460        request.setBody(urlParameters);
461
462        String json;
463        try {
464            BoxJSONResponse response = (BoxJSONResponse) request.send();
465            json = response.getJSON();
466        } catch (BoxAPIException e) {
467            this.notifyError(e);
468            this.refreshLock.writeLock().unlock();
469            throw e;
470        }
471
472        JsonObject jsonObject = JsonObject.readFrom(json);
473        this.accessToken = jsonObject.get("access_token").asString();
474        this.refreshToken = jsonObject.get("refresh_token").asString();
475        this.lastRefresh = System.currentTimeMillis();
476        this.expires = jsonObject.get("expires_in").asLong() * 1000;
477
478        this.notifyRefresh();
479
480        this.refreshLock.writeLock().unlock();
481    }
482
483    /**
484     * Restores a saved connection state into this BoxAPIConnection.
485     *
486     * @see    #save
487     * @param  state the saved state that was created with {@link #save}.
488     */
489    public void restore(String state) {
490        JsonObject json = JsonObject.readFrom(state);
491        String accessToken = json.get("accessToken").asString();
492        String refreshToken = json.get("refreshToken").asString();
493        long lastRefresh = json.get("lastRefresh").asLong();
494        long expires = json.get("expires").asLong();
495        String userAgent = json.get("userAgent").asString();
496        String tokenURL = json.get("tokenURL").asString();
497        String baseURL = json.get("baseURL").asString();
498        String baseUploadURL = json.get("baseUploadURL").asString();
499        boolean autoRefresh = json.get("autoRefresh").asBoolean();
500        int maxRequestAttempts = json.get("maxRequestAttempts").asInt();
501
502        this.accessToken = accessToken;
503        this.refreshToken = refreshToken;
504        this.lastRefresh = lastRefresh;
505        this.expires = expires;
506        this.userAgent = userAgent;
507        this.tokenURL = tokenURL;
508        this.baseURL = baseURL;
509        this.baseUploadURL = baseUploadURL;
510        this.autoRefresh = autoRefresh;
511        this.maxRequestAttempts = maxRequestAttempts;
512    }
513
514    /**
515     * Notifies a refresh event to all the listeners.
516     */
517    protected void notifyRefresh() {
518        for (BoxAPIConnectionListener listener : this.listeners) {
519            listener.onRefresh(this);
520        }
521    }
522
523    /**
524     * Notifies an error event to all the listeners.
525     * @param error A BoxAPIException instance.
526     */
527    protected void notifyError(BoxAPIException error) {
528        for (BoxAPIConnectionListener listener : this.listeners) {
529            listener.onError(this, error);
530        }
531    }
532
533    /**
534     * Add a listener to listen to Box API connection events.
535     * @param listener a listener to listen to Box API connection.
536     */
537    public void addListener(BoxAPIConnectionListener listener) {
538        this.listeners.add(listener);
539    }
540
541    /**
542     * Remove a listener listening to Box API connection events.
543     * @param listener the listener to remove.
544     */
545    public void removeListener(BoxAPIConnectionListener listener) {
546        this.listeners.remove(listener);
547    }
548
549    /**
550     * Gets the RequestInterceptor associated with this API connection.
551     * @return the RequestInterceptor associated with this API connection.
552     */
553    public RequestInterceptor getRequestInterceptor() {
554        return this.interceptor;
555    }
556
557    /**
558     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
559     * @param interceptor the RequestInterceptor.
560     */
561    public void setRequestInterceptor(RequestInterceptor interceptor) {
562        this.interceptor = interceptor;
563    }
564
565    /**
566     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
567     *
568     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
569     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
570     * have to manually configure it again after restoring the connection.</p>
571     *
572     * @see    #restore
573     * @return the state of this connection.
574     */
575    public String save() {
576        JsonObject state = new JsonObject()
577            .add("accessToken", this.accessToken)
578            .add("refreshToken", this.refreshToken)
579            .add("lastRefresh", this.lastRefresh)
580            .add("expires", this.expires)
581            .add("userAgent", this.userAgent)
582            .add("tokenURL", this.tokenURL)
583            .add("baseURL", this.baseURL)
584            .add("baseUploadURL", this.baseUploadURL)
585            .add("autoRefresh", this.autoRefresh)
586            .add("maxRequestAttempts", this.maxRequestAttempts);
587        return state.toString();
588    }
589
590    String lockAccessToken() {
591        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
592            this.refreshLock.writeLock().lock();
593            try {
594                if (this.needsRefresh()) {
595                    this.refresh();
596                }
597                this.refreshLock.readLock().lock();
598            } finally {
599                this.refreshLock.writeLock().unlock();
600            }
601        } else {
602            this.refreshLock.readLock().lock();
603        }
604
605        return this.accessToken;
606    }
607
608    void unlockAccessToken() {
609        this.refreshLock.readLock().unlock();
610    }
611}