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