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