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