001package com.box.sdk; 002 003import static java.lang.String.join; 004 005import com.eclipsesource.json.Json; 006import com.eclipsesource.json.JsonObject; 007import com.eclipsesource.json.JsonValue; 008import java.net.MalformedURLException; 009import java.net.Proxy; 010import java.net.URI; 011import java.net.URL; 012import java.util.ArrayList; 013import java.util.HashMap; 014import java.util.List; 015import java.util.Map; 016import java.util.Optional; 017import java.util.concurrent.locks.ReadWriteLock; 018import java.util.concurrent.locks.ReentrantReadWriteLock; 019import java.util.regex.Pattern; 020 021/** 022 * Represents an authenticated connection to the Box API. 023 * 024 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be 025 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of 026 * BoxAPIConnection may be created to support multi-user login.</p> 027 */ 028public class BoxAPIConnection { 029 /** 030 * The default total maximum number of times an API request will be tried when error responses 031 * are received. 032 * 033 * @deprecated DEFAULT_MAX_RETRIES is preferred because it more clearly sets the number 034 * of times a request should be retried after an error response is received. 035 */ 036 @Deprecated 037 public static final int DEFAULT_MAX_ATTEMPTS = 5; 038 039 /** 040 * The default maximum number of times an API request will be retried after an error response 041 * is received. 042 */ 043 public static final int DEFAULT_MAX_RETRIES = 5; 044 045 /** 046 * Default authorization URL 047 */ 048 protected static final String DEFAULT_BASE_AUTHORIZATION_URL = "https://account.box.com/api/"; 049 050 static final String AS_USER_HEADER = "As-User"; 051 052 private static final String API_VERSION = "2.0"; 053 private static final String OAUTH_SUFFIX = "oauth2/authorize"; 054 private static final String TOKEN_URL_SUFFIX = "oauth2/token"; 055 private static final String REVOKE_URL_SUFFIX = "oauth2/revoke"; 056 private static final String DEFAULT_BASE_URL = "https://api.box.com/"; 057 private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/"; 058 private static final String DEFAULT_BASE_APP_URL = "https://app.box.com"; 059 060 private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications"; 061 062 private static final String JAVA_VERSION = System.getProperty("java.version"); 063 private static final String SDK_VERSION = "3.2.0"; 064 065 /** 066 * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For 067 * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed. 068 */ 069 private static final long REFRESH_EPSILON = 60000; 070 071 private final String clientID; 072 private final String clientSecret; 073 private final ReadWriteLock refreshLock; 074 075 // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in 076 // the double-checked lock in getAccessToken(), they must be atomic. 077 private volatile long lastRefresh; 078 private volatile long expires; 079 080 private Proxy proxy; 081 private String proxyUsername; 082 private String proxyPassword; 083 084 private String userAgent; 085 private String accessToken; 086 private String refreshToken; 087 private String tokenURL; 088 private String revokeURL; 089 private String baseURL; 090 private String baseUploadURL; 091 private String baseAppURL; 092 private String baseAuthorizationURL; 093 private boolean autoRefresh; 094 private int maxRetryAttempts; 095 private int connectTimeout; 096 private int readTimeout; 097 private final List<BoxAPIConnectionListener> listeners; 098 private RequestInterceptor interceptor; 099 private final Map<String, String> customHeaders; 100 101 /** 102 * Constructs a new BoxAPIConnection that authenticates with a developer or access token. 103 * 104 * @param accessToken a developer or access token to use for authenticating with the API. 105 */ 106 public BoxAPIConnection(String accessToken) { 107 this(null, null, accessToken, null); 108 } 109 110 /** 111 * Constructs a new BoxAPIConnection with an access token that can be refreshed. 112 * 113 * @param clientID the client ID to use when refreshing the access token. 114 * @param clientSecret the client secret to use when refreshing the access token. 115 * @param accessToken an initial access token to use for authenticating with the API. 116 * @param refreshToken an initial refresh token to use when refreshing the access token. 117 */ 118 public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) { 119 this.clientID = clientID; 120 this.clientSecret = clientSecret; 121 this.accessToken = accessToken; 122 this.refreshToken = refreshToken; 123 this.baseURL = fixBaseUrl(DEFAULT_BASE_URL); 124 this.baseUploadURL = fixBaseUrl(DEFAULT_BASE_UPLOAD_URL); 125 this.baseAppURL = DEFAULT_BASE_APP_URL; 126 this.baseAuthorizationURL = DEFAULT_BASE_AUTHORIZATION_URL; 127 this.autoRefresh = true; 128 this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts(); 129 this.connectTimeout = BoxGlobalSettings.getConnectTimeout(); 130 this.readTimeout = BoxGlobalSettings.getReadTimeout(); 131 this.refreshLock = new ReentrantReadWriteLock(); 132 this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")"; 133 this.listeners = new ArrayList<>(); 134 this.customHeaders = new HashMap<>(); 135 } 136 137 /** 138 * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth. 139 * 140 * @param clientID the client ID to use when exchanging the auth code for an access token. 141 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 142 * @param authCode an auth code obtained from the first half of the OAuth process. 143 */ 144 public BoxAPIConnection(String clientID, String clientSecret, String authCode) { 145 this(clientID, clientSecret, null, null); 146 this.authenticate(authCode); 147 } 148 149 /** 150 * Constructs a new BoxAPIConnection. 151 * 152 * @param clientID the client ID to use when exchanging the auth code for an access token. 153 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 154 */ 155 public BoxAPIConnection(String clientID, String clientSecret) { 156 this(clientID, clientSecret, null, null); 157 } 158 159 /** 160 * Constructs a new BoxAPIConnection levaraging BoxConfig. 161 * 162 * @param boxConfig BoxConfig file, which should have clientId and clientSecret 163 */ 164 public BoxAPIConnection(BoxConfig boxConfig) { 165 this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null); 166 } 167 168 /** 169 * Restores a BoxAPIConnection from a saved state. 170 * 171 * @param clientID the client ID to use with the connection. 172 * @param clientSecret the client secret to use with the connection. 173 * @param state the saved state that was created with {@link #save}. 174 * @return a restored API connection. 175 * @see #save 176 */ 177 public static BoxAPIConnection restore(String clientID, String clientSecret, String state) { 178 BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret); 179 api.restore(state); 180 return api; 181 } 182 183 /** 184 * Returns the default authorization URL which is used to perform the authorization_code based OAuth2 flow. 185 * If custom Authorization URL is needed use instance method {@link BoxAPIConnection#getAuthorizationURL} 186 * 187 * @param clientID the client ID to use with the connection. 188 * @param redirectUri the URL to which Box redirects the browser when authentication completes. 189 * @param state the text string that you choose. 190 * Box sends the same string to your redirect URL when authentication is complete. 191 * @param scopes this optional parameter identifies the Box scopes available 192 * to the application once it's authenticated. 193 * @return the authorization URL 194 */ 195 public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) { 196 return createFullAuthorizationUrl(DEFAULT_BASE_AUTHORIZATION_URL, clientID, redirectUri, state, scopes); 197 } 198 199 private static URL createFullAuthorizationUrl( 200 String authorizationUrl, String clientID, URI redirectUri, String state, List<String> scopes 201 ) { 202 URLTemplate template = new URLTemplate(authorizationUrl + OAUTH_SUFFIX); 203 QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID) 204 .appendParam("response_type", "code") 205 .appendParam("redirect_uri", redirectUri.toString()) 206 .appendParam("state", state); 207 208 if (scopes != null && !scopes.isEmpty()) { 209 queryBuilder.appendParam("scope", join(" ", scopes)); 210 } 211 212 return template.buildWithQuery("", queryBuilder.toString()); 213 } 214 215 /** 216 * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained 217 * from the first half of OAuth. 218 * 219 * @param authCode the auth code obtained from the first half of the OAuth process. 220 */ 221 public void authenticate(String authCode) { 222 URL url; 223 try { 224 url = new URL(this.tokenURL); 225 } catch (MalformedURLException e) { 226 assert false : "An invalid token URL indicates a bug in the SDK."; 227 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 228 } 229 230 String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s", 231 authCode, this.clientID, this.clientSecret); 232 233 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 234 request.shouldAuthenticate(false); 235 request.setBody(urlParameters); 236 237 BoxJSONResponse response = (BoxJSONResponse) request.send(); 238 String json = response.getJSON(); 239 240 JsonObject jsonObject = Json.parse(json).asObject(); 241 this.accessToken = jsonObject.get("access_token").asString(); 242 this.refreshToken = jsonObject.get("refresh_token").asString(); 243 this.lastRefresh = System.currentTimeMillis(); 244 this.expires = jsonObject.get("expires_in").asLong() * 1000; 245 } 246 247 /** 248 * Gets the client ID. 249 * 250 * @return the client ID. 251 */ 252 public String getClientID() { 253 return this.clientID; 254 } 255 256 /** 257 * Gets the client secret. 258 * 259 * @return the client secret. 260 */ 261 public String getClientSecret() { 262 return this.clientSecret; 263 } 264 265 /** 266 * Gets the amount of time for which this connection's access token is valid. 267 * 268 * @return the amount of time in milliseconds. 269 */ 270 public long getExpires() { 271 return this.expires; 272 } 273 274 /** 275 * Sets the amount of time for which this connection's access token is valid before it must be refreshed. 276 * 277 * @param milliseconds the number of milliseconds for which the access token is valid. 278 */ 279 public void setExpires(long milliseconds) { 280 this.expires = milliseconds; 281 } 282 283 /** 284 * Gets the token URL that's used to request access tokens. The default value is 285 * "https://www.box.com/api/oauth2/token". 286 * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#TOKEN_URL_SUFFIX}. 287 * 288 * @return the token URL. 289 */ 290 public String getTokenURL() { 291 if (this.tokenURL != null) { 292 return this.tokenURL; 293 } else { 294 return this.baseURL + TOKEN_URL_SUFFIX; 295 } 296 } 297 298 /** 299 * Sets the token URL that's used to request access tokens. For example, the default token URL is 300 * "https://www.box.com/api/oauth2/token". 301 * 302 * @param tokenURL the token URL. 303 * @deprecated Use {@link BoxAPIConnection#setBaseURL(String)} 304 */ 305 public void setTokenURL(String tokenURL) { 306 this.tokenURL = tokenURL; 307 } 308 309 /** 310 * Returns the URL used for token revocation. 311 * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#REVOKE_URL_SUFFIX}. 312 * 313 * @return The url used for token revocation. 314 */ 315 public String getRevokeURL() { 316 if (this.revokeURL != null) { 317 return this.revokeURL; 318 } else { 319 return this.baseURL + REVOKE_URL_SUFFIX; 320 } 321 } 322 323 /** 324 * Set the URL used for token revocation. 325 * 326 * @param url The url to use. 327 * @deprecated Use {@link BoxAPIConnection#setBaseURL(String)} 328 */ 329 public void setRevokeURL(String url) { 330 this.revokeURL = url; 331 } 332 333 /** 334 * Gets the base URL that's used when sending requests to the Box API. 335 * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#API_VERSION}. 336 * The default value is "https://api.box.com/2.0/". 337 * 338 * @return the base URL. 339 */ 340 public String getBaseURL() { 341 return this.baseURL + API_VERSION + "/"; 342 } 343 344 /** 345 * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is 346 * "https://api.box.com/". This method changes how {@link BoxAPIConnection#getRevokeURL()} 347 * and {@link BoxAPIConnection#getTokenURL()} are constructed. 348 * 349 * @param baseURL a base URL 350 */ 351 public void setBaseURL(String baseURL) { 352 this.baseURL = fixBaseUrl(baseURL); 353 } 354 355 /** 356 * Gets the base upload URL that's used when performing file uploads to Box. 357 * The URL is created from {@link BoxAPIConnection#baseUploadURL} and {@link BoxAPIConnection#API_VERSION}. 358 * 359 * @return the base upload URL. 360 */ 361 public String getBaseUploadURL() { 362 return this.baseUploadURL + API_VERSION + "/"; 363 } 364 365 /** 366 * Sets the base upload URL to be used when performing file uploads to Box. 367 * 368 * @param baseUploadURL a base upload URL. 369 */ 370 public void setBaseUploadURL(String baseUploadURL) { 371 this.baseUploadURL = fixBaseUrl(baseUploadURL); 372 } 373 374 /** 375 * Returns the authorization URL which is used to perform the authorization_code based OAuth2 flow. 376 * The URL is created from {@link BoxAPIConnection#baseAuthorizationURL} and {@link BoxAPIConnection#OAUTH_SUFFIX}. 377 * 378 * @param redirectUri the URL to which Box redirects the browser when authentication completes. 379 * @param state the text string that you choose. 380 * Box sends the same string to your redirect URL when authentication is complete. 381 * @param scopes this optional parameter identifies the Box scopes available 382 * to the application once it's authenticated. 383 * @return the authorization URL 384 */ 385 public URL getAuthorizationURL(URI redirectUri, String state, List<String> scopes) { 386 return createFullAuthorizationUrl( 387 this.baseAuthorizationURL + OAUTH_SUFFIX, this.clientID, redirectUri, state, scopes 388 ); 389 } 390 391 /** 392 * Sets authorization base URL which is used to perform the authorization_code based OAuth2 flow. 393 * 394 * @param baseAuthorizationURL Authorization URL. Default value is https://account.box.com/api/. 395 */ 396 public void setBaseAuthorizationURL(String baseAuthorizationURL) { 397 this.baseAuthorizationURL = fixBaseUrl(baseAuthorizationURL); 398 } 399 400 /** 401 * Gets the user agent that's used when sending requests to the Box API. 402 * 403 * @return the user agent. 404 */ 405 public String getUserAgent() { 406 return this.userAgent; 407 } 408 409 /** 410 * Sets the user agent to be used when sending requests to the Box API. 411 * 412 * @param userAgent the user agent. 413 */ 414 public void setUserAgent(String userAgent) { 415 this.userAgent = userAgent; 416 } 417 418 /** 419 * Gets the base App url. Used for e.g. file requests. 420 * 421 * @return the base App Url. 422 */ 423 public String getBaseAppUrl() { 424 return this.baseAppURL; 425 } 426 427 /** 428 * Sets the base App url. Used for e.g. file requests. 429 * 430 * @param baseAppURL a base App Url. 431 */ 432 public void setBaseAppUrl(String baseAppURL) { 433 this.baseAppURL = baseAppURL; 434 } 435 436 /** 437 * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the 438 * access token if it has expired since the last call to <code>getAccessToken()</code>. 439 * 440 * @return a valid access token that can be used to authenticate an API request. 441 */ 442 public String getAccessToken() { 443 if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) { 444 this.refreshLock.writeLock().lock(); 445 try { 446 if (this.needsRefresh()) { 447 this.refresh(); 448 } 449 } finally { 450 this.refreshLock.writeLock().unlock(); 451 } 452 } 453 454 return this.accessToken; 455 } 456 457 /** 458 * Sets the access token to use when authenticating API requests. 459 * 460 * @param accessToken a valid access token to use when authenticating API requests. 461 */ 462 public void setAccessToken(String accessToken) { 463 this.accessToken = accessToken; 464 } 465 466 /** 467 * Gets the refresh lock to be used when refreshing an access token. 468 * 469 * @return the refresh lock. 470 */ 471 protected ReadWriteLock getRefreshLock() { 472 return this.refreshLock; 473 } 474 475 /** 476 * Gets a refresh token that can be used to refresh an access token. 477 * 478 * @return a valid refresh token. 479 */ 480 public String getRefreshToken() { 481 return this.refreshToken; 482 } 483 484 /** 485 * Sets the refresh token to use when refreshing an access token. 486 * 487 * @param refreshToken a valid refresh token. 488 */ 489 public void setRefreshToken(String refreshToken) { 490 this.refreshToken = refreshToken; 491 } 492 493 /** 494 * Gets the last time that the access token was refreshed. 495 * 496 * @return the last refresh time in milliseconds. 497 */ 498 public long getLastRefresh() { 499 return this.lastRefresh; 500 } 501 502 /** 503 * Sets the last time that the access token was refreshed. 504 * 505 * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since 506 * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p> 507 * 508 * @param lastRefresh the new last refresh time in milliseconds. 509 */ 510 public void setLastRefresh(long lastRefresh) { 511 this.lastRefresh = lastRefresh; 512 } 513 514 /** 515 * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true. 516 * 517 * @return true if auto token refresh is enabled; otherwise false. 518 */ 519 public boolean getAutoRefresh() { 520 return this.autoRefresh; 521 } 522 523 /** 524 * Enables or disables automatic refreshing of this connection's access token. Defaults to true. 525 * 526 * @param autoRefresh true to enable auto token refresh; otherwise false. 527 */ 528 public void setAutoRefresh(boolean autoRefresh) { 529 this.autoRefresh = autoRefresh; 530 } 531 532 /** 533 * Sets the total maximum number of times an API request will be tried when error responses 534 * are received. 535 * 536 * @return the maximum number of request attempts. 537 * @deprecated getMaxRetryAttempts is preferred because it more clearly gets the number 538 * of times a request should be retried after an error response is received. 539 */ 540 @Deprecated 541 public int getMaxRequestAttempts() { 542 return this.maxRetryAttempts + 1; 543 } 544 545 /** 546 * Sets the total maximum number of times an API request will be tried when error responses 547 * are received. 548 * 549 * @param attempts the maximum number of request attempts. 550 * @deprecated setMaxRetryAttempts is preferred because it more clearly sets the number 551 * of times a request should be retried after an error response is received. 552 */ 553 @Deprecated 554 public void setMaxRequestAttempts(int attempts) { 555 this.maxRetryAttempts = attempts - 1; 556 } 557 558 /** 559 * Gets the maximum number of times an API request will be retried after an error response 560 * is received. 561 * 562 * @return the maximum number of request attempts. 563 */ 564 public int getMaxRetryAttempts() { 565 return this.maxRetryAttempts; 566 } 567 568 /** 569 * Sets the maximum number of times an API request will be retried after an error response 570 * is received. 571 * 572 * @param attempts the maximum number of request attempts. 573 */ 574 public void setMaxRetryAttempts(int attempts) { 575 this.maxRetryAttempts = attempts; 576 } 577 578 /** 579 * Gets the connect timeout for this connection in milliseconds. 580 * 581 * @return the number of milliseconds to connect before timing out. 582 */ 583 public int getConnectTimeout() { 584 return this.connectTimeout; 585 } 586 587 /** 588 * Sets the connect timeout for this connection. 589 * 590 * @param connectTimeout The number of milliseconds to wait for the connection to be established. 591 */ 592 public void setConnectTimeout(int connectTimeout) { 593 this.connectTimeout = connectTimeout; 594 } 595 596 /** 597 * Gets the read timeout for this connection in milliseconds. 598 * 599 * @return the number of milliseconds to wait for bytes to be read before timing out. 600 */ 601 public int getReadTimeout() { 602 return this.readTimeout; 603 } 604 605 /** 606 * Sets the read timeout for this connection. 607 * 608 * @param readTimeout The number of milliseconds to wait for bytes to be read. 609 */ 610 public void setReadTimeout(int readTimeout) { 611 this.readTimeout = readTimeout; 612 } 613 614 /** 615 * Gets the proxy value to use for API calls to Box. 616 * 617 * @return the current proxy. 618 */ 619 public Proxy getProxy() { 620 return this.proxy; 621 } 622 623 /** 624 * Sets the proxy to use for API calls to Box. 625 * 626 * @param proxy the proxy to use for API calls to Box. 627 */ 628 public void setProxy(Proxy proxy) { 629 this.proxy = proxy; 630 } 631 632 /** 633 * Gets the username to use for a proxy that requires basic auth. 634 * 635 * @return the username to use for a proxy that requires basic auth. 636 */ 637 public String getProxyUsername() { 638 return this.proxyUsername; 639 } 640 641 /** 642 * Sets the username to use for a proxy that requires basic auth. 643 * 644 * @param proxyUsername the username to use for a proxy that requires basic auth. 645 */ 646 public void setProxyUsername(String proxyUsername) { 647 this.proxyUsername = proxyUsername; 648 } 649 650 /** 651 * Gets the password to use for a proxy that requires basic auth. 652 * 653 * @return the password to use for a proxy that requires basic auth. 654 */ 655 public String getProxyPassword() { 656 return this.proxyPassword; 657 } 658 659 /** 660 * Sets the password to use for a proxy that requires basic auth. 661 * 662 * @param proxyPassword the password to use for a proxy that requires basic auth. 663 */ 664 public void setProxyPassword(String proxyPassword) { 665 this.proxyPassword = proxyPassword; 666 } 667 668 /** 669 * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh 670 * token was never set. 671 * 672 * @return true if the access token can be refreshed; otherwise false. 673 */ 674 public boolean canRefresh() { 675 return this.refreshToken != null; 676 } 677 678 /** 679 * Determines if this connection's access token has expired and needs to be refreshed. 680 * 681 * @return true if the access token needs to be refreshed; otherwise false. 682 */ 683 public boolean needsRefresh() { 684 boolean needsRefresh; 685 686 this.refreshLock.readLock().lock(); 687 long now = System.currentTimeMillis(); 688 long tokenDuration = (now - this.lastRefresh); 689 needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON); 690 this.refreshLock.readLock().unlock(); 691 692 return needsRefresh; 693 } 694 695 /** 696 * Refresh's this connection's access token using its refresh token. 697 * 698 * @throws IllegalStateException if this connection's access token cannot be refreshed. 699 */ 700 public void refresh() { 701 this.refreshLock.writeLock().lock(); 702 703 if (!this.canRefresh()) { 704 this.refreshLock.writeLock().unlock(); 705 throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a " 706 + "refresh token."); 707 } 708 709 URL url; 710 try { 711 url = new URL(getTokenURL()); 712 } catch (MalformedURLException e) { 713 this.refreshLock.writeLock().unlock(); 714 assert false : "An invalid refresh URL indicates a bug in the SDK."; 715 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 716 } 717 718 BoxAPIRequest request = createTokenRequest(url); 719 720 String json; 721 try { 722 BoxAPIResponse boxAPIResponse = request.send(); 723 BoxJSONResponse response = (BoxJSONResponse) boxAPIResponse; 724 json = response.getJSON(); 725 } catch (BoxAPIException e) { 726 this.refreshLock.writeLock().unlock(); 727 this.notifyError(e); 728 throw e; 729 } 730 731 try { 732 extractTokens(Json.parse(json).asObject()); 733 734 this.notifyRefresh(); 735 } finally { 736 this.refreshLock.writeLock().unlock(); 737 } 738 } 739 740 /** 741 * Restores a saved connection state into this BoxAPIConnection. 742 * 743 * @param state the saved state that was created with {@link #save}. 744 * @see #save 745 */ 746 public void restore(String state) { 747 JsonObject json = Json.parse(state).asObject(); 748 String accessToken = json.get("accessToken").asString(); 749 String refreshToken = json.get("refreshToken").asString(); 750 long lastRefresh = json.get("lastRefresh").asLong(); 751 long expires = json.get("expires").asLong(); 752 String userAgent = json.get("userAgent").asString(); 753 String tokenURL = getKeyValueOrDefault(json, "tokenURL", null); 754 String revokeURL = getKeyValueOrDefault(json, "revokeURL", null); 755 String baseURL = json.get("baseURL").asString(); 756 String baseUploadURL = json.get("baseUploadURL").asString(); 757 String authorizationURL = getKeyValueOrDefault(json, "authorizationURL", DEFAULT_BASE_AUTHORIZATION_URL); 758 boolean autoRefresh = json.get("autoRefresh").asBoolean(); 759 760 // Try to read deprecated value 761 int maxRequestAttempts = -1; 762 if (json.names().contains("maxRequestAttempts")) { 763 maxRequestAttempts = json.get("maxRequestAttempts").asInt(); 764 } 765 766 int maxRetryAttempts = -1; 767 if (json.names().contains("maxRetryAttempts")) { 768 maxRetryAttempts = json.get("maxRetryAttempts").asInt(); 769 } 770 771 this.accessToken = accessToken; 772 this.refreshToken = refreshToken; 773 this.lastRefresh = lastRefresh; 774 this.expires = expires; 775 this.userAgent = userAgent; 776 this.tokenURL = tokenURL; 777 this.revokeURL = revokeURL; 778 this.setBaseURL(baseURL); 779 this.setBaseUploadURL(baseUploadURL); 780 this.setBaseAuthorizationURL(authorizationURL); 781 this.autoRefresh = autoRefresh; 782 783 // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts" 784 if (maxRequestAttempts > -1) { 785 this.maxRetryAttempts = maxRequestAttempts - 1; 786 } else { 787 this.maxRetryAttempts = maxRetryAttempts; 788 } 789 790 } 791 792 protected String getKeyValueOrDefault(JsonObject json, String key, String defaultValue) { 793 return Optional.ofNullable(json.get(key)) 794 .filter(js -> !js.isNull()) 795 .map(JsonValue::asString) 796 .orElse(defaultValue); 797 } 798 799 /** 800 * Notifies a refresh event to all the listeners. 801 */ 802 protected void notifyRefresh() { 803 for (BoxAPIConnectionListener listener : this.listeners) { 804 listener.onRefresh(this); 805 } 806 } 807 808 /** 809 * Notifies an error event to all the listeners. 810 * 811 * @param error A BoxAPIException instance. 812 */ 813 protected void notifyError(BoxAPIException error) { 814 for (BoxAPIConnectionListener listener : this.listeners) { 815 listener.onError(this, error); 816 } 817 } 818 819 /** 820 * Add a listener to listen to Box API connection events. 821 * 822 * @param listener a listener to listen to Box API connection. 823 */ 824 public void addListener(BoxAPIConnectionListener listener) { 825 this.listeners.add(listener); 826 } 827 828 /** 829 * Remove a listener listening to Box API connection events. 830 * 831 * @param listener the listener to remove. 832 */ 833 public void removeListener(BoxAPIConnectionListener listener) { 834 this.listeners.remove(listener); 835 } 836 837 /** 838 * Gets the RequestInterceptor associated with this API connection. 839 * 840 * @return the RequestInterceptor associated with this API connection. 841 */ 842 public RequestInterceptor getRequestInterceptor() { 843 return this.interceptor; 844 } 845 846 /** 847 * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API. 848 * 849 * @param interceptor the RequestInterceptor. 850 */ 851 public void setRequestInterceptor(RequestInterceptor interceptor) { 852 this.interceptor = interceptor; 853 } 854 855 /** 856 * Get a lower-scoped token restricted to a resource for the list of scopes that are passed. 857 * 858 * @param scopes the list of scopes to which the new token should be restricted for 859 * @param resource the resource for which the new token has to be obtained 860 * @return scopedToken which has access token and other details 861 * @throws BoxAPIException if resource is not a valid Box API endpoint or shared link 862 */ 863 public ScopedToken getLowerScopedToken(List<String> scopes, String resource) { 864 assert (scopes != null); 865 assert (scopes.size() > 0); 866 URL url; 867 try { 868 url = new URL(this.getTokenURL()); 869 } catch (MalformedURLException e) { 870 assert false : "An invalid refresh URL indicates a bug in the SDK."; 871 throw new BoxAPIException("An invalid refresh URL indicates a bug in the SDK.", e); 872 } 873 874 StringBuilder spaceSeparatedScopes = this.buildScopesForTokenDownscoping(scopes); 875 876 String urlParameters = String.format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange" 877 + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s" 878 + "&scope=%s", 879 this.getAccessToken(), spaceSeparatedScopes); 880 881 if (resource != null) { 882 883 ResourceLinkType resourceType = this.determineResourceLinkType(resource); 884 885 if (resourceType == ResourceLinkType.APIEndpoint) { 886 urlParameters = String.format(urlParameters + "&resource=%s", resource); 887 } else if (resourceType == ResourceLinkType.SharedLink) { 888 urlParameters = String.format(urlParameters + "&box_shared_link=%s", resource); 889 } else if (resourceType == ResourceLinkType.Unknown) { 890 String argExceptionMessage = String.format("Unable to determine resource type: %s", resource); 891 BoxAPIException e = new BoxAPIException(argExceptionMessage); 892 this.notifyError(e); 893 throw e; 894 } else { 895 String argExceptionMessage = String.format("Unhandled resource type: %s", resource); 896 BoxAPIException e = new BoxAPIException(argExceptionMessage); 897 this.notifyError(e); 898 throw e; 899 } 900 } 901 902 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 903 request.shouldAuthenticate(false); 904 request.setBody(urlParameters); 905 906 String jsonResponse; 907 try { 908 BoxJSONResponse response = (BoxJSONResponse) request.send(); 909 jsonResponse = response.getJSON(); 910 } catch (BoxAPIException e) { 911 this.notifyError(e); 912 throw e; 913 } 914 915 JsonObject jsonObject = Json.parse(jsonResponse).asObject(); 916 ScopedToken token = new ScopedToken(jsonObject); 917 token.setObtainedAt(System.currentTimeMillis()); 918 token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000); 919 return token; 920 } 921 922 /** 923 * Convert List<String> to space-delimited String. 924 * Needed for versions prior to Java 8, which don't have String.join(delimiter, list) 925 * 926 * @param scopes the list of scopes to read from 927 * @return space-delimited String of scopes 928 */ 929 private StringBuilder buildScopesForTokenDownscoping(List<String> scopes) { 930 StringBuilder spaceSeparatedScopes = new StringBuilder(); 931 for (int i = 0; i < scopes.size(); i++) { 932 spaceSeparatedScopes.append(scopes.get(i)); 933 if (i < scopes.size() - 1) { 934 spaceSeparatedScopes.append(" "); 935 } 936 } 937 938 return spaceSeparatedScopes; 939 } 940 941 /** 942 * Determines the type of resource, given a link to a Box resource. 943 * 944 * @param resourceLink the resource URL to check 945 * @return ResourceLinkType that categorizes the provided resourceLink 946 */ 947 protected ResourceLinkType determineResourceLinkType(String resourceLink) { 948 949 ResourceLinkType resourceType = ResourceLinkType.Unknown; 950 951 try { 952 URL validUrl = new URL(resourceLink); 953 String validURLStr = validUrl.toString(); 954 final String apiFilesEndpointPattern = ".*box.com/2.0/files/\\d+"; 955 final String apiFoldersEndpointPattern = ".*box.com/2.0/folders/\\d+"; 956 final String sharedLinkPattern = "(.*box.com/s/.*|.*box.com.*s=.*)"; 957 958 if (Pattern.matches(apiFilesEndpointPattern, validURLStr) 959 || Pattern.matches(apiFoldersEndpointPattern, validURLStr)) { 960 resourceType = ResourceLinkType.APIEndpoint; 961 } else if (Pattern.matches(sharedLinkPattern, validURLStr)) { 962 resourceType = ResourceLinkType.SharedLink; 963 } 964 } catch (MalformedURLException e) { 965 //Swallow exception and return default ResourceLinkType set at top of function 966 } 967 968 return resourceType; 969 } 970 971 /** 972 * Revokes the tokens associated with this API connection. This results in the connection no 973 * longer being able to make API calls until a fresh authorization is made by calling authenticate() 974 */ 975 public void revokeToken() { 976 977 URL url; 978 try { 979 url = new URL(getRevokeURL()); 980 } catch (MalformedURLException e) { 981 assert false : "An invalid refresh URL indicates a bug in the SDK."; 982 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 983 } 984 985 String urlParameters = String.format("token=%s&client_id=%s&client_secret=%s", 986 this.accessToken, this.clientID, this.clientSecret); 987 988 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 989 request.shouldAuthenticate(false); 990 request.setBody(urlParameters); 991 992 request.send(); 993 } 994 995 /** 996 * Saves the state of this connection to a string so that it can be persisted and restored at a later time. 997 * 998 * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns 999 * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will 1000 * have to manually configure it again after restoring the connection.</p> 1001 * 1002 * @return the state of this connection. 1003 * @see #restore 1004 */ 1005 public String save() { 1006 JsonObject state = new JsonObject() 1007 .add("accessToken", this.accessToken) 1008 .add("refreshToken", this.refreshToken) 1009 .add("lastRefresh", this.lastRefresh) 1010 .add("expires", this.expires) 1011 .add("userAgent", this.userAgent) 1012 .add("tokenURL", this.tokenURL) 1013 .add("revokeURL", this.revokeURL) 1014 .add("baseURL", this.baseURL) 1015 .add("baseUploadURL", this.baseUploadURL) 1016 .add("authorizationURL", this.baseAuthorizationURL) 1017 .add("autoRefresh", this.autoRefresh) 1018 .add("maxRetryAttempts", this.maxRetryAttempts); 1019 return state.toString(); 1020 } 1021 1022 String lockAccessToken() { 1023 if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) { 1024 this.refreshLock.writeLock().lock(); 1025 try { 1026 if (this.needsRefresh()) { 1027 this.refresh(); 1028 } 1029 this.refreshLock.readLock().lock(); 1030 } finally { 1031 this.refreshLock.writeLock().unlock(); 1032 } 1033 } else { 1034 this.refreshLock.readLock().lock(); 1035 } 1036 1037 return this.accessToken; 1038 } 1039 1040 void unlockAccessToken() { 1041 this.refreshLock.readLock().unlock(); 1042 } 1043 1044 /** 1045 * Get the value for the X-Box-UA header. 1046 * 1047 * @return the header value. 1048 */ 1049 String getBoxUAHeader() { 1050 1051 return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION; 1052 } 1053 1054 /** 1055 * Sets a custom header to be sent on all requests through this API connection. 1056 * 1057 * @param header the header name. 1058 * @param value the header value. 1059 */ 1060 public void setCustomHeader(String header, String value) { 1061 this.customHeaders.put(header, value); 1062 } 1063 1064 /** 1065 * Removes a custom header, so it will no longer be sent on requests through this API connection. 1066 * 1067 * @param header the header name. 1068 */ 1069 public void removeCustomHeader(String header) { 1070 this.customHeaders.remove(header); 1071 } 1072 1073 /** 1074 * Suppresses email notifications from API actions. This is typically used by security or admin applications 1075 * to prevent spamming end users when doing automated processing on their content. 1076 */ 1077 public void suppressNotifications() { 1078 this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off"); 1079 } 1080 1081 /** 1082 * Re-enable email notifications from API actions if they have been suppressed. 1083 * 1084 * @see #suppressNotifications 1085 */ 1086 public void enableNotifications() { 1087 this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER); 1088 } 1089 1090 /** 1091 * Set this API connection to make API calls on behalf of another users, impersonating them. This 1092 * functionality can only be used by admins and service accounts. 1093 * 1094 * @param userID the ID of the user to act as. 1095 */ 1096 public void asUser(String userID) { 1097 this.setCustomHeader(AS_USER_HEADER, userID); 1098 } 1099 1100 /** 1101 * Sets this API connection to make API calls on behalf of the user with whom the access token is associated. 1102 * This undoes any previous calls to asUser(). 1103 * 1104 * @see #asUser 1105 */ 1106 public void asSelf() { 1107 this.removeCustomHeader(AS_USER_HEADER); 1108 } 1109 1110 Map<String, String> getHeaders() { 1111 return this.customHeaders; 1112 } 1113 1114 protected void extractTokens(JsonObject jsonObject) { 1115 this.accessToken = jsonObject.get("access_token").asString(); 1116 this.refreshToken = jsonObject.get("refresh_token").asString(); 1117 this.lastRefresh = System.currentTimeMillis(); 1118 this.expires = jsonObject.get("expires_in").asLong() * 1000; 1119 } 1120 1121 protected BoxAPIRequest createTokenRequest(URL url) { 1122 String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", 1123 this.refreshToken, this.clientID, this.clientSecret); 1124 1125 BoxAPIRequest request = new BoxAPIRequest(this, url, "POST"); 1126 request.shouldAuthenticate(false); 1127 request.setBody(urlParameters); 1128 return request; 1129 } 1130 1131 private String fixBaseUrl(String baseUrl) { 1132 return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"; 1133 } 1134 1135 /** 1136 * Used to categorize the types of resource links. 1137 */ 1138 protected enum ResourceLinkType { 1139 /** 1140 * Catch-all default for resource links that are unknown. 1141 */ 1142 Unknown, 1143 1144 /** 1145 * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id. 1146 */ 1147 APIEndpoint, 1148 1149 /** 1150 * Resource URLs that point to a resource that has been shared 1151 * such as https://example.box.com/s/qwertyuiop1234567890asdfghjk 1152 * or https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk. 1153 */ 1154 SharedLink 1155 } 1156}