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