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 v0.7.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     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
107     * from the first half of OAuth.
108     * @param authCode the auth code obtained from the first half of the OAuth process.
109     */
110    public void authenticate(String authCode) {
111        URL url = null;
112        try {
113            url = new URL(this.tokenURL);
114        } catch (MalformedURLException e) {
115            assert false : "An invalid token URL indicates a bug in the SDK.";
116            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
117        }
118
119        String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
120            authCode, this.clientID, this.clientSecret);
121
122        BoxAPIRequest request = new BoxAPIRequest(url, "POST");
123        request.addHeader("Content-Type", "application/x-www-form-urlencoded");
124        request.setBody(urlParameters);
125
126        BoxJSONResponse response = (BoxJSONResponse) request.send();
127        String json = response.getJSON();
128
129        JsonObject jsonObject = JsonObject.readFrom(json);
130        this.accessToken = jsonObject.get("access_token").asString();
131        this.refreshToken = jsonObject.get("refresh_token").asString();
132        this.lastRefresh = System.currentTimeMillis();
133        this.expires = jsonObject.get("expires_in").asLong() * 1000;
134    }
135
136    /**
137     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
138     * @param milliseconds the number of milliseconds for which the access token is valid.
139     */
140    public void setExpires(long milliseconds) {
141        this.expires = milliseconds;
142    }
143
144    /**
145     * Gets the amount of time for which this connection's access token is valid.
146     * @return the amount of time in milliseconds.
147     */
148    public long getExpires() {
149        return this.expires;
150    }
151
152    /**
153     * Gets the token URL that's used to request access tokens.  The default value is
154     * "https://www.box.com/api/oauth2/token".
155     * @return the token URL.
156     */
157    public String getTokenURL() {
158        return this.tokenURL;
159    }
160
161    /**
162     * Sets the token URL that's used to request access tokens.  For example, the default token URL is
163     * "https://www.box.com/api/oauth2/token".
164     * @param tokenURL the token URL.
165     */
166    public void setTokenURL(String tokenURL) {
167        this.tokenURL = tokenURL;
168    }
169
170    /**
171     * Gets the base URL that's used when sending requests to the Box API. The default value is
172     * "https://api.box.com/2.0/".
173     * @return the base URL.
174     */
175    public String getBaseURL() {
176        return this.baseURL;
177    }
178
179    /**
180     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
181     * "https://api.box.com/2.0/".
182     * @param baseURL a base URL
183     */
184    public void setBaseURL(String baseURL) {
185        this.baseURL = baseURL;
186    }
187
188    /**
189     * Gets the base upload URL that's used when performing file uploads to Box.
190     * @return the base upload URL.
191     */
192    public String getBaseUploadURL() {
193        return this.baseUploadURL;
194    }
195
196    /**
197     * Sets the base upload URL to be used when performing file uploads to Box.
198     * @param baseUploadURL a base upload URL.
199     */
200    public void setBaseUploadURL(String baseUploadURL) {
201        this.baseUploadURL = baseUploadURL;
202    }
203
204    /**
205     * Gets the user agent that's used when sending requests to the Box API.
206     * @return the user agent.
207     */
208    public String getUserAgent() {
209        return this.userAgent;
210    }
211
212    /**
213     * Sets the user agent to be used when sending requests to the Box API.
214     * @param userAgent the user agent.
215     */
216    public void setUserAgent(String userAgent) {
217        this.userAgent = userAgent;
218    }
219
220    /**
221     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
222     * access token if it has expired since the last call to <code>getAccessToken()</code>.
223     * @return a valid access token that can be used to authenticate an API request.
224     */
225    public String getAccessToken() {
226        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
227            this.refreshLock.writeLock().lock();
228            try {
229                if (this.needsRefresh()) {
230                    this.refresh();
231                }
232            } finally {
233                this.refreshLock.writeLock().unlock();
234            }
235        }
236
237        return this.accessToken;
238    }
239
240    /**
241     * Sets the access token to use when authenticating API requests.
242     * @param accessToken a valid access token to use when authenticating API requests.
243     */
244    public void setAccessToken(String accessToken) {
245        this.accessToken = accessToken;
246    }
247
248    /**
249     * Gets a refresh token that can be used to refresh an access token.
250     * @return a valid refresh token.
251     */
252    public String getRefreshToken() {
253        return this.refreshToken;
254    }
255
256    /**
257     * Sets the refresh token to use when refreshing an access token.
258     * @param refreshToken a valid refresh token.
259     */
260    public void setRefreshToken(String refreshToken) {
261        this.refreshToken = refreshToken;
262    }
263
264    /**
265     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
266     * @param autoRefresh true to enable auto token refresh; otherwise false.
267     */
268    public void setAutoRefresh(boolean autoRefresh) {
269        this.autoRefresh = autoRefresh;
270    }
271
272    /**
273     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
274     * @return true if auto token refresh is enabled; otherwise false.
275     */
276    public boolean getAutoRefresh() {
277        return this.autoRefresh;
278    }
279
280    /**
281     * Gets the maximum number of times an API request will be tried when an error occurs.
282     * @return the maximum number of request attempts.
283     */
284    public int getMaxRequestAttempts() {
285        return this.maxRequestAttempts;
286    }
287
288    /**
289     * Sets the maximum number of times an API request will be tried when an error occurs.
290     * @param attempts the maximum number of request attempts.
291     */
292    public void setMaxRequestAttempts(int attempts) {
293        this.maxRequestAttempts = attempts;
294    }
295
296    /**
297     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
298     * token was never set.
299     * @return true if the access token can be refreshed; otherwise false.
300     */
301    public boolean canRefresh() {
302        return this.refreshToken != null;
303    }
304
305    /**
306     * Determines if this connection's access token has expired and needs to be refreshed.
307     * @return true if the access token needs to be refreshed; otherwise false.
308     */
309    public boolean needsRefresh() {
310        boolean needsRefresh;
311
312        this.refreshLock.readLock().lock();
313        if (this.expires == 0) {
314            needsRefresh = false;
315        } else {
316            long now = System.currentTimeMillis();
317            long tokenDuration = (now - this.lastRefresh);
318            needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
319        }
320        this.refreshLock.readLock().unlock();
321
322        return needsRefresh;
323    }
324
325    /**
326     * Refresh's this connection's access token using its refresh token.
327     * @throws IllegalStateException if this connection's access token cannot be refreshed.
328     */
329    public void refresh() {
330        this.refreshLock.writeLock().lock();
331
332        if (!this.canRefresh()) {
333            this.refreshLock.writeLock().unlock();
334            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
335                + "refresh token.");
336        }
337
338        URL url = null;
339        try {
340            url = new URL(this.tokenURL);
341        } catch (MalformedURLException e) {
342            this.refreshLock.writeLock().unlock();
343            assert false : "An invalid refresh URL indicates a bug in the SDK.";
344            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
345        }
346
347        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
348            this.refreshToken, this.clientID, this.clientSecret);
349
350        BoxAPIRequest request = new BoxAPIRequest(url, "POST");
351        request.addHeader("Content-Type", "application/x-www-form-urlencoded");
352        request.setBody(urlParameters);
353
354        String json;
355        try {
356            BoxJSONResponse response = (BoxJSONResponse) request.send();
357            json = response.getJSON();
358        } catch (BoxAPIException e) {
359            this.refreshLock.writeLock().unlock();
360            throw e;
361        }
362
363        JsonObject jsonObject = JsonObject.readFrom(json);
364        this.accessToken = jsonObject.get("access_token").asString();
365        this.refreshToken = jsonObject.get("refresh_token").asString();
366        this.lastRefresh = System.currentTimeMillis();
367        this.expires = jsonObject.get("expires_in").asLong() * 1000;
368
369        this.notifyRefresh();
370
371        this.refreshLock.writeLock().unlock();
372    }
373
374    /**
375     * Notifies refresh event to all the listeners.
376     */
377    private void notifyRefresh() {
378        for (BoxAPIConnectionListener listener : this.listeners) {
379            listener.onRefresh();
380        }
381    }
382
383    /**
384     * Add a listener to listen to Box API connection events.
385     * @param listener a listener to listen to Box API connection.
386     */
387    public void addListener(BoxAPIConnectionListener listener) {
388        this.listeners.add(listener);
389    }
390
391    /**
392     * Remove a listener listening to Box API connection events.
393     * @param listener the listener to remove.
394     */
395    public void removeListener(BoxAPIConnectionListener listener) {
396        this.listeners.remove(listener);
397    }
398
399    /**
400     * Gets the RequestInterceptor associated with this API connection.
401     * @return the RequestInterceptor associated with this API connection.
402     */
403    public RequestInterceptor getRequestInterceptor() {
404        return this.interceptor;
405    }
406
407    /**
408     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
409     * @param interceptor the RequestInterceptor.
410     */
411    public void setRequestInterceptor(RequestInterceptor interceptor) {
412        this.interceptor = interceptor;
413    }
414}