001package com.box.sdk;
002
003import java.net.MalformedURLException;
004import java.net.URL;
005
006import com.eclipsesource.json.JsonObject;
007
008/**
009 * Represents an authenticated connection to the Box API.
010 *
011 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
012 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
013 * BoxAPIConnection may be created to support multi-user login.</p>
014 */
015public class BoxAPIConnection {
016    /**
017     * The default maximum number of times an API request will be tried when an error occurs.
018     */
019    public static final int DEFAULT_MAX_ATTEMPTS = 3;
020
021    private static final String TOKEN_URL_STRING = "https://www.box.com/api/oauth2/token";
022    private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/";
023    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/";
024
025    /**
026     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
027     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
028     */
029    private static final long REFRESH_EPSILON = 60000;
030
031    private final String clientID;
032    private final String clientSecret;
033
034    private long lastRefresh;
035    private long expires;
036    private String baseURL;
037    private String baseUploadURL;
038    private String accessToken;
039    private String refreshToken;
040    private boolean autoRefresh;
041    private int maxRequestAttempts;
042
043    /**
044     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
045     * @param  accessToken a developer or access token to use for authenticating with the API.
046     */
047    public BoxAPIConnection(String accessToken) {
048        this(null, null, accessToken, null);
049    }
050
051    /**
052     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
053     * @param  clientID     the client ID to use when refreshing the access token.
054     * @param  clientSecret the client secret to use when refreshing the access token.
055     * @param  accessToken  an initial access token to use for authenticating with the API.
056     * @param  refreshToken an initial refresh token to use when refreshing the access token.
057     */
058    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
059        this.clientID = clientID;
060        this.clientSecret = clientSecret;
061        this.accessToken = accessToken;
062        this.setRefreshToken(refreshToken);
063        this.baseURL = DEFAULT_BASE_URL;
064        this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL;
065        this.autoRefresh = true;
066        this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS;
067    }
068
069    /**
070     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
071     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
072     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
073     * @param  authCode     an auth code obtained from the first half of the OAuth process.
074     */
075    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
076        this(clientID, clientSecret, null, null);
077
078        URL url = null;
079        try {
080            url = new URL(TOKEN_URL_STRING);
081        } catch (MalformedURLException e) {
082            assert false : "An invalid token URL indicates a bug in the SDK.";
083            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
084        }
085
086        String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
087            authCode, clientID, clientSecret);
088
089        BoxAPIRequest request = new BoxAPIRequest(url, "POST");
090        request.addHeader("Content-Type", "application/x-www-form-urlencoded");
091        request.setBody(urlParameters);
092
093        BoxJSONResponse response = (BoxJSONResponse) request.send();
094        String json = response.getJSON();
095
096        JsonObject jsonObject = JsonObject.readFrom(json);
097        this.accessToken = jsonObject.get("access_token").asString();
098        this.setRefreshToken(jsonObject.get("refresh_token").asString());
099        this.expires = jsonObject.get("expires_in").asLong() * 1000;
100    }
101
102    /**
103     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
104     * @param milliseconds the number of milliseconds for which the access token is valid.
105     */
106    public void setExpires(long milliseconds) {
107        this.expires = milliseconds;
108    }
109
110    /**
111     * Gets the amount of time for which this connection's access token is valid.
112     * @return the amount of time in milliseconds.
113     */
114    public long getExpires() {
115        return this.expires;
116    }
117
118    /**
119     * Gets the base URL that's used when sending requests to the Box API. The default value is
120     * "https://api.box.com/2.0/".
121     * @return the base URL.
122     */
123    public String getBaseURL() {
124        return this.baseURL;
125    }
126
127    /**
128     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
129     * "https://api.box.com/2.0/".
130     * @param baseURL a base URL
131     */
132    public void setBaseURL(String baseURL) {
133        this.baseURL = baseURL;
134    }
135
136    /**
137     * Gets the base upload URL that's used when performing file uploads to Box.
138     * @return the base upload URL.
139     */
140    public String getBaseUploadURL() {
141        return this.baseUploadURL;
142    }
143
144    /**
145     * Sets the base upload URL to be used when performing file uploads to Box.
146     * @param baseUploadURL a base upload URL.
147     */
148    public void setBaseUploadURL(String baseUploadURL) {
149        this.baseUploadURL = baseUploadURL;
150    }
151
152    /**
153     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
154     * access token if it has expired since the last call to <code>getAccessToken()</code>.
155     * @return a valid access token that can be used to authenticate an API request.
156     */
157    public String getAccessToken() {
158        if (this.canRefresh() && this.needsRefresh() && this.autoRefresh) {
159            this.refresh();
160        }
161
162        return this.accessToken;
163    }
164
165    /**
166     * Sets the access token to use when authenticating API requests.
167     * @param accessToken a valid access token to use when authenticating API requests.
168     */
169    public void setAccessToken(String accessToken) {
170        this.accessToken = accessToken;
171    }
172
173    /**
174     * Gets a refresh token that can be used to refresh an access token.
175     * @return a valid refresh token.
176     */
177    public String getRefreshToken() {
178        return this.refreshToken;
179    }
180
181    /**
182     * Sets the refresh token to use when refreshing an access token.
183     * @param refreshToken a valid refresh token.
184     */
185    public void setRefreshToken(String refreshToken) {
186        this.refreshToken = refreshToken;
187        this.lastRefresh = System.currentTimeMillis();
188    }
189
190    /**
191     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
192     * @param autoRefresh true to enable auto token refresh; otherwise false.
193     */
194    public void setAutoRefresh(boolean autoRefresh) {
195        this.autoRefresh = autoRefresh;
196    }
197
198    /**
199     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
200     * @return true if auto token refresh is enabled; otherwise false.
201     */
202    public boolean getAutoRefresh() {
203        return this.autoRefresh;
204    }
205
206    /**
207     * Gets the maximum number of times an API request will be tried when an error occurs.
208     * @return the maximum number of request attempts.
209     */
210    public int getMaxRequestAttempts() {
211        return this.maxRequestAttempts;
212    }
213
214    /**
215     * Sets the maximum number of times an API request will be tried when an error occurs.
216     * @param attempts the maximum number of request attempts.
217     */
218    public void setMaxRequestAttempts(int attempts) {
219        this.maxRequestAttempts = attempts;
220    }
221
222    /**
223     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
224     * token was never set.
225     * @return true if the access token can be refreshed; otherwise false.
226     */
227    public boolean canRefresh() {
228        return this.refreshToken != null;
229    }
230
231    /**
232     * Determines if this connection's access token has expired and needs to be refreshed.
233     * @return true if the access token needs to be refreshed; otherwise false.
234     */
235    public boolean needsRefresh() {
236        if (this.expires == 0) {
237            return false;
238        }
239
240        long now = System.currentTimeMillis();
241        long tokenDuration = (now - this.lastRefresh);
242        return (tokenDuration >= this.expires - REFRESH_EPSILON);
243    }
244
245    /**
246     * Refresh's this connection's access token using its refresh token.
247     * @throws IllegalStateException if this connection's access token cannot be refreshed.
248     */
249    public void refresh() {
250        if (!this.canRefresh()) {
251            throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
252                + "refresh token.");
253        }
254
255        URL url = null;
256        try {
257            url = new URL(TOKEN_URL_STRING);
258        } catch (MalformedURLException e) {
259            assert false : "An invalid refresh URL indicates a bug in the SDK.";
260            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
261        }
262
263        String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
264            this.refreshToken, this.clientID, this.clientSecret);
265
266        BoxAPIRequest request = new BoxAPIRequest(url, "POST");
267        request.addHeader("Content-Type", "application/x-www-form-urlencoded");
268        request.setBody(urlParameters);
269
270        BoxJSONResponse response = (BoxJSONResponse) request.send();
271        String json = response.getJSON();
272
273        JsonObject jsonObject = JsonObject.readFrom(json);
274        this.accessToken = jsonObject.get("access_token").asString();
275        this.refreshToken = jsonObject.get("refresh_token").asString();
276        this.expires = jsonObject.get("expires_in").asLong() * 1000;
277    }
278}