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