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