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