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