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}