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://www.box.com/api/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 v1.1.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     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
158     * @param milliseconds the number of milliseconds for which the access token is valid.
159     */
160    public void setExpires(long milliseconds) {
161        this.expires = milliseconds;
162    }
163
164    /**
165     * Gets the amount of time for which this connection's access token is valid.
166     * @return the amount of time in milliseconds.
167     */
168    public long getExpires() {
169        return this.expires;
170    }
171
172    /**
173     * Gets the token URL that's used to request access tokens.  The default value is
174     * "https://www.box.com/api/oauth2/token".
175     * @return the token URL.
176     */
177    public String getTokenURL() {
178        return this.tokenURL;
179    }
180
181    /**
182     * Sets the token URL that's used to request access tokens.  For example, the default token URL is
183     * "https://www.box.com/api/oauth2/token".
184     * @param tokenURL the token URL.
185     */
186    public void setTokenURL(String tokenURL) {
187        this.tokenURL = tokenURL;
188    }
189
190    /**
191     * Gets the base URL that's used when sending requests to the Box API. The default value is
192     * "https://api.box.com/2.0/".
193     * @return the base URL.
194     */
195    public String getBaseURL() {
196        return this.baseURL;
197    }
198
199    /**
200     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
201     * "https://api.box.com/2.0/".
202     * @param baseURL a base URL
203     */
204    public void setBaseURL(String baseURL) {
205        this.baseURL = baseURL;
206    }
207
208    /**
209     * Gets the base upload URL that's used when performing file uploads to Box.
210     * @return the base upload URL.
211     */
212    public String getBaseUploadURL() {
213        return this.baseUploadURL;
214    }
215
216    /**
217     * Sets the base upload URL to be used when performing file uploads to Box.
218     * @param baseUploadURL a base upload URL.
219     */
220    public void setBaseUploadURL(String baseUploadURL) {
221        this.baseUploadURL = baseUploadURL;
222    }
223
224    /**
225     * Gets the user agent that's used when sending requests to the Box API.
226     * @return the user agent.
227     */
228    public String getUserAgent() {
229        return this.userAgent;
230    }
231
232    /**
233     * Sets the user agent to be used when sending requests to the Box API.
234     * @param userAgent the user agent.
235     */
236    public void setUserAgent(String userAgent) {
237        this.userAgent = userAgent;
238    }
239
240    /**
241     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
242     * access token if it has expired since the last call to <code>getAccessToken()</code>.
243     * @return a valid access token that can be used to authenticate an API request.
244     */
245    public String getAccessToken() {
246        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
247            this.refreshLock.writeLock().lock();
248            try {
249                if (this.needsRefresh()) {
250                    this.refresh();
251                }
252            } finally {
253                this.refreshLock.writeLock().unlock();
254            }
255        }
256
257        return this.accessToken;
258    }
259
260    /**
261     * Sets the access token to use when authenticating API requests.
262     * @param accessToken a valid access token to use when authenticating API requests.
263     */
264    public void setAccessToken(String accessToken) {
265        this.accessToken = accessToken;
266    }
267
268    /**
269     * Gets a refresh token that can be used to refresh an access token.
270     * @return a valid refresh token.
271     */
272    public String getRefreshToken() {
273        return this.refreshToken;
274    }
275
276    /**
277     * Sets the refresh token to use when refreshing an access token.
278     * @param refreshToken a valid refresh token.
279     */
280    public void setRefreshToken(String refreshToken) {
281        this.refreshToken = refreshToken;
282    }
283
284    /**
285     * Gets the last time that the access token was refreshed.
286     *
287     * @return the last refresh time in milliseconds.
288     */
289    public long getLastRefresh() {
290        return this.lastRefresh;
291    }
292
293    /**
294     * Sets the last time that the access token was refreshed.
295     *
296     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
297     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
298     *
299     * @param lastRefresh the new last refresh time in milliseconds.
300     */
301    public void setLastRefresh(long lastRefresh) {
302        this.lastRefresh = lastRefresh;
303    }
304
305    /**
306     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
307     * @param autoRefresh true to enable auto token refresh; otherwise false.
308     */
309    public void setAutoRefresh(boolean autoRefresh) {
310        this.autoRefresh = autoRefresh;
311    }
312
313    /**
314     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
315     * @return true if auto token refresh is enabled; otherwise false.
316     */
317    public boolean getAutoRefresh() {
318        return this.autoRefresh;
319    }
320
321    /**
322     * Gets the maximum number of times an API request will be tried when an error occurs.
323     * @return the maximum number of request attempts.
324     */
325    public int getMaxRequestAttempts() {
326        return this.maxRequestAttempts;
327    }
328
329    /**
330     * Sets the maximum number of times an API request will be tried when an error occurs.
331     * @param attempts the maximum number of request attempts.
332     */
333    public void setMaxRequestAttempts(int attempts) {
334        this.maxRequestAttempts = attempts;
335    }
336
337    /**
338     * Gets the proxy value to use for API calls to Box.
339     * @return the current proxy.
340     */
341    public Proxy getProxy() {
342        return this.proxy;
343    }
344
345    /**
346     * Sets the proxy to use for API calls to Box.
347     * @param proxy the proxy to use for API calls to Box.
348     */
349    public void setProxy(Proxy proxy) {
350        this.proxy = proxy;
351    }
352
353    /**
354     * Gets the username to use for a proxy that requires basic auth.
355     * @return the username to use for a proxy that requires basic auth.
356     */
357    public String getProxyUsername() {
358        return this.proxyUsername;
359    }
360
361    /**
362     * Sets the username to use for a proxy that requires basic auth.
363     * @param proxyUsername the username to use for a proxy that requires basic auth.
364     */
365    public void setProxyUsername(String proxyUsername) {
366        this.proxyUsername = proxyUsername;
367    }
368
369    /**
370     * Gets the password to use for a proxy that requires basic auth.
371     * @return the password to use for a proxy that requires basic auth.
372     */
373    public String getProxyPassword() {
374        return this.proxyPassword;
375    }
376
377    /**
378     * Sets the password to use for a proxy that requires basic auth.
379     * @param proxyPassword the password to use for a proxy that requires basic auth.
380     */
381    public void setProxyPassword(String proxyPassword) {
382        this.proxyPassword = proxyPassword;
383    }
384
385    /**
386     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
387     * token was never set.
388     * @return true if the access token can be refreshed; otherwise false.
389     */
390    public boolean canRefresh() {
391        return this.refreshToken != null;
392    }
393
394    /**
395     * Determines if this connection's access token has expired and needs to be refreshed.
396     * @return true if the access token needs to be refreshed; otherwise false.
397     */
398    public boolean needsRefresh() {
399        boolean needsRefresh;
400
401        this.refreshLock.readLock().lock();
402        long now = System.currentTimeMillis();
403        long tokenDuration = (now - this.lastRefresh);
404        needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
405        this.refreshLock.readLock().unlock();
406
407        return needsRefresh;
408    }
409
410    /**
411     * Refresh's this connection's access token using its refresh token.
412     * @throws IllegalStateException if this connection's access token cannot be refreshed.
413     */
414    public void refresh() {
415        this.refreshLock.writeLock().lock();
416
417        if (!this.canRefresh()) {
418            this.refreshLock.writeLock().unlock();
419            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
420                + "refresh token.");
421        }
422
423        URL url = null;
424        try {
425            url = new URL(this.tokenURL);
426        } catch (MalformedURLException e) {
427            this.refreshLock.writeLock().unlock();
428            assert false : "An invalid refresh URL indicates a bug in the SDK.";
429            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
430        }
431
432        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
433            this.refreshToken, this.clientID, this.clientSecret);
434
435        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
436        request.shouldAuthenticate(false);
437        request.setBody(urlParameters);
438
439        String json;
440        try {
441            BoxJSONResponse response = (BoxJSONResponse) request.send();
442            json = response.getJSON();
443        } catch (BoxAPIException e) {
444            this.notifyError(e);
445            this.refreshLock.writeLock().unlock();
446            throw e;
447        }
448
449        JsonObject jsonObject = JsonObject.readFrom(json);
450        this.accessToken = jsonObject.get("access_token").asString();
451        this.refreshToken = jsonObject.get("refresh_token").asString();
452        this.lastRefresh = System.currentTimeMillis();
453        this.expires = jsonObject.get("expires_in").asLong() * 1000;
454
455        this.notifyRefresh();
456
457        this.refreshLock.writeLock().unlock();
458    }
459
460    /**
461     * Restores a saved connection state into this BoxAPIConnection.
462     *
463     * @see    #save
464     * @param  state the saved state that was created with {@link #save}.
465     */
466    public void restore(String state) {
467        JsonObject json = JsonObject.readFrom(state);
468        String accessToken = json.get("accessToken").asString();
469        String refreshToken = json.get("refreshToken").asString();
470        long lastRefresh = json.get("lastRefresh").asLong();
471        long expires = json.get("expires").asLong();
472        String userAgent = json.get("userAgent").asString();
473        String tokenURL = json.get("tokenURL").asString();
474        String baseURL = json.get("baseURL").asString();
475        String baseUploadURL = json.get("baseUploadURL").asString();
476        boolean autoRefresh = json.get("autoRefresh").asBoolean();
477        int maxRequestAttempts = json.get("maxRequestAttempts").asInt();
478
479        this.accessToken = accessToken;
480        this.refreshToken = refreshToken;
481        this.lastRefresh = lastRefresh;
482        this.expires = expires;
483        this.userAgent = userAgent;
484        this.tokenURL = tokenURL;
485        this.baseURL = baseURL;
486        this.baseUploadURL = baseUploadURL;
487        this.autoRefresh = autoRefresh;
488        this.maxRequestAttempts = maxRequestAttempts;
489    }
490
491    /**
492     * Notifies a refresh event to all the listeners.
493     */
494    private void notifyRefresh() {
495        for (BoxAPIConnectionListener listener : this.listeners) {
496            listener.onRefresh(this);
497        }
498    }
499
500    /**
501     * Notifies an error event to all the listeners.
502     */
503    private void notifyError(BoxAPIException error) {
504        for (BoxAPIConnectionListener listener : this.listeners) {
505            listener.onError(this, error);
506        }
507    }
508
509    /**
510     * Add a listener to listen to Box API connection events.
511     * @param listener a listener to listen to Box API connection.
512     */
513    public void addListener(BoxAPIConnectionListener listener) {
514        this.listeners.add(listener);
515    }
516
517    /**
518     * Remove a listener listening to Box API connection events.
519     * @param listener the listener to remove.
520     */
521    public void removeListener(BoxAPIConnectionListener listener) {
522        this.listeners.remove(listener);
523    }
524
525    /**
526     * Gets the RequestInterceptor associated with this API connection.
527     * @return the RequestInterceptor associated with this API connection.
528     */
529    public RequestInterceptor getRequestInterceptor() {
530        return this.interceptor;
531    }
532
533    /**
534     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
535     * @param interceptor the RequestInterceptor.
536     */
537    public void setRequestInterceptor(RequestInterceptor interceptor) {
538        this.interceptor = interceptor;
539    }
540
541    /**
542     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
543     *
544     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
545     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
546     * have to manually configure it again after restoring the connection.</p>
547     *
548     * @see    #restore
549     * @return the state of this connection.
550     */
551    public String save() {
552        JsonObject state = new JsonObject()
553            .add("accessToken", this.accessToken)
554            .add("refreshToken", this.refreshToken)
555            .add("lastRefresh", this.lastRefresh)
556            .add("expires", this.expires)
557            .add("userAgent", this.userAgent)
558            .add("tokenURL", this.tokenURL)
559            .add("baseURL", this.baseURL)
560            .add("baseUploadURL", this.baseUploadURL)
561            .add("autoRefresh", this.autoRefresh)
562            .add("maxRequestAttempts", this.maxRequestAttempts);
563        return state.toString();
564    }
565
566    String lockAccessToken() {
567        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
568            this.refreshLock.writeLock().lock();
569            try {
570                if (this.needsRefresh()) {
571                    this.refresh();
572                }
573                this.refreshLock.readLock().lock();
574            } finally {
575                this.refreshLock.writeLock().unlock();
576            }
577        } else {
578            this.refreshLock.readLock().lock();
579        }
580
581        return this.accessToken;
582    }
583
584    void unlockAccessToken() {
585        this.refreshLock.readLock().unlock();
586    }
587}