001package com.box.sdk; 002 003import java.net.MalformedURLException; 004import java.net.URL; 005import java.util.ArrayList; 006import java.util.List; 007import java.util.concurrent.locks.ReadWriteLock; 008import java.util.concurrent.locks.ReentrantReadWriteLock; 009 010import com.eclipsesource.json.JsonObject; 011 012/** 013 * Represents an authenticated connection to the Box API. 014 * 015 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be 016 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of 017 * BoxAPIConnection may be created to support multi-user login.</p> 018 */ 019public class BoxAPIConnection { 020 /** 021 * The default maximum number of times an API request will be tried when an error occurs. 022 */ 023 public static final int DEFAULT_MAX_ATTEMPTS = 3; 024 025 private static final String TOKEN_URL_STRING = "https://www.box.com/api/oauth2/token"; 026 private static final String DEFAULT_BASE_URL = "https://api.box.com/2.0/"; 027 private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/2.0/"; 028 029 /** 030 * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For 031 * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed. 032 */ 033 private static final long REFRESH_EPSILON = 60000; 034 035 private final String clientID; 036 private final String clientSecret; 037 private final ReadWriteLock refreshLock; 038 039 // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in 040 // the double-checked lock in getAccessToken(), they must be atomic. 041 private volatile long lastRefresh; 042 private volatile long expires; 043 044 private String userAgent; 045 private String accessToken; 046 private String refreshToken; 047 private String tokenURL; 048 private String baseURL; 049 private String baseUploadURL; 050 private boolean autoRefresh; 051 private int maxRequestAttempts; 052 private List<BoxAPIConnectionListener> listeners; 053 private RequestInterceptor interceptor; 054 055 /** 056 * Constructs a new BoxAPIConnection that authenticates with a developer or access token. 057 * @param accessToken a developer or access token to use for authenticating with the API. 058 */ 059 public BoxAPIConnection(String accessToken) { 060 this(null, null, accessToken, null); 061 } 062 063 /** 064 * Constructs a new BoxAPIConnection with an access token that can be refreshed. 065 * @param clientID the client ID to use when refreshing the access token. 066 * @param clientSecret the client secret to use when refreshing the access token. 067 * @param accessToken an initial access token to use for authenticating with the API. 068 * @param refreshToken an initial refresh token to use when refreshing the access token. 069 */ 070 public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) { 071 this.clientID = clientID; 072 this.clientSecret = clientSecret; 073 this.accessToken = accessToken; 074 this.refreshToken = refreshToken; 075 this.tokenURL = TOKEN_URL_STRING; 076 this.baseURL = DEFAULT_BASE_URL; 077 this.baseUploadURL = DEFAULT_BASE_UPLOAD_URL; 078 this.autoRefresh = true; 079 this.maxRequestAttempts = DEFAULT_MAX_ATTEMPTS; 080 this.refreshLock = new ReentrantReadWriteLock(); 081 this.userAgent = "Box Java SDK v0.7.0"; 082 this.listeners = new ArrayList<BoxAPIConnectionListener>(); 083 } 084 085 /** 086 * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth. 087 * @param clientID the client ID to use when exchanging the auth code for an access token. 088 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 089 * @param authCode an auth code obtained from the first half of the OAuth process. 090 */ 091 public BoxAPIConnection(String clientID, String clientSecret, String authCode) { 092 this(clientID, clientSecret, null, null); 093 this.authenticate(authCode); 094 } 095 096 /** 097 * Constructs a new BoxAPIConnection. 098 * @param clientID the client ID to use when exchanging the auth code for an access token. 099 * @param clientSecret the client secret to use when exchanging the auth code for an access token. 100 */ 101 public BoxAPIConnection(String clientID, String clientSecret) { 102 this(clientID, clientSecret, null, null); 103 } 104 105 /** 106 * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained 107 * from the first half of OAuth. 108 * @param authCode the auth code obtained from the first half of the OAuth process. 109 */ 110 public void authenticate(String authCode) { 111 URL url = null; 112 try { 113 url = new URL(this.tokenURL); 114 } catch (MalformedURLException e) { 115 assert false : "An invalid token URL indicates a bug in the SDK."; 116 throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e); 117 } 118 119 String urlParameters = String.format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s", 120 authCode, this.clientID, this.clientSecret); 121 122 BoxAPIRequest request = new BoxAPIRequest(url, "POST"); 123 request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 124 request.setBody(urlParameters); 125 126 BoxJSONResponse response = (BoxJSONResponse) request.send(); 127 String json = response.getJSON(); 128 129 JsonObject jsonObject = JsonObject.readFrom(json); 130 this.accessToken = jsonObject.get("access_token").asString(); 131 this.refreshToken = jsonObject.get("refresh_token").asString(); 132 this.lastRefresh = System.currentTimeMillis(); 133 this.expires = jsonObject.get("expires_in").asLong() * 1000; 134 } 135 136 /** 137 * Sets the amount of time for which this connection's access token is valid before it must be refreshed. 138 * @param milliseconds the number of milliseconds for which the access token is valid. 139 */ 140 public void setExpires(long milliseconds) { 141 this.expires = milliseconds; 142 } 143 144 /** 145 * Gets the amount of time for which this connection's access token is valid. 146 * @return the amount of time in milliseconds. 147 */ 148 public long getExpires() { 149 return this.expires; 150 } 151 152 /** 153 * Gets the token URL that's used to request access tokens. The default value is 154 * "https://www.box.com/api/oauth2/token". 155 * @return the token URL. 156 */ 157 public String getTokenURL() { 158 return this.tokenURL; 159 } 160 161 /** 162 * Sets the token URL that's used to request access tokens. For example, the default token URL is 163 * "https://www.box.com/api/oauth2/token". 164 * @param tokenURL the token URL. 165 */ 166 public void setTokenURL(String tokenURL) { 167 this.tokenURL = tokenURL; 168 } 169 170 /** 171 * Gets the base URL that's used when sending requests to the Box API. The default value is 172 * "https://api.box.com/2.0/". 173 * @return the base URL. 174 */ 175 public String getBaseURL() { 176 return this.baseURL; 177 } 178 179 /** 180 * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is 181 * "https://api.box.com/2.0/". 182 * @param baseURL a base URL 183 */ 184 public void setBaseURL(String baseURL) { 185 this.baseURL = baseURL; 186 } 187 188 /** 189 * Gets the base upload URL that's used when performing file uploads to Box. 190 * @return the base upload URL. 191 */ 192 public String getBaseUploadURL() { 193 return this.baseUploadURL; 194 } 195 196 /** 197 * Sets the base upload URL to be used when performing file uploads to Box. 198 * @param baseUploadURL a base upload URL. 199 */ 200 public void setBaseUploadURL(String baseUploadURL) { 201 this.baseUploadURL = baseUploadURL; 202 } 203 204 /** 205 * Gets the user agent that's used when sending requests to the Box API. 206 * @return the user agent. 207 */ 208 public String getUserAgent() { 209 return this.userAgent; 210 } 211 212 /** 213 * Sets the user agent to be used when sending requests to the Box API. 214 * @param userAgent the user agent. 215 */ 216 public void setUserAgent(String userAgent) { 217 this.userAgent = userAgent; 218 } 219 220 /** 221 * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the 222 * access token if it has expired since the last call to <code>getAccessToken()</code>. 223 * @return a valid access token that can be used to authenticate an API request. 224 */ 225 public String getAccessToken() { 226 if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) { 227 this.refreshLock.writeLock().lock(); 228 try { 229 if (this.needsRefresh()) { 230 this.refresh(); 231 } 232 } finally { 233 this.refreshLock.writeLock().unlock(); 234 } 235 } 236 237 return this.accessToken; 238 } 239 240 /** 241 * Sets the access token to use when authenticating API requests. 242 * @param accessToken a valid access token to use when authenticating API requests. 243 */ 244 public void setAccessToken(String accessToken) { 245 this.accessToken = accessToken; 246 } 247 248 /** 249 * Gets a refresh token that can be used to refresh an access token. 250 * @return a valid refresh token. 251 */ 252 public String getRefreshToken() { 253 return this.refreshToken; 254 } 255 256 /** 257 * Sets the refresh token to use when refreshing an access token. 258 * @param refreshToken a valid refresh token. 259 */ 260 public void setRefreshToken(String refreshToken) { 261 this.refreshToken = refreshToken; 262 } 263 264 /** 265 * Enables or disables automatic refreshing of this connection's access token. Defaults to true. 266 * @param autoRefresh true to enable auto token refresh; otherwise false. 267 */ 268 public void setAutoRefresh(boolean autoRefresh) { 269 this.autoRefresh = autoRefresh; 270 } 271 272 /** 273 * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true. 274 * @return true if auto token refresh is enabled; otherwise false. 275 */ 276 public boolean getAutoRefresh() { 277 return this.autoRefresh; 278 } 279 280 /** 281 * Gets the maximum number of times an API request will be tried when an error occurs. 282 * @return the maximum number of request attempts. 283 */ 284 public int getMaxRequestAttempts() { 285 return this.maxRequestAttempts; 286 } 287 288 /** 289 * Sets the maximum number of times an API request will be tried when an error occurs. 290 * @param attempts the maximum number of request attempts. 291 */ 292 public void setMaxRequestAttempts(int attempts) { 293 this.maxRequestAttempts = attempts; 294 } 295 296 /** 297 * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh 298 * token was never set. 299 * @return true if the access token can be refreshed; otherwise false. 300 */ 301 public boolean canRefresh() { 302 return this.refreshToken != null; 303 } 304 305 /** 306 * Determines if this connection's access token has expired and needs to be refreshed. 307 * @return true if the access token needs to be refreshed; otherwise false. 308 */ 309 public boolean needsRefresh() { 310 boolean needsRefresh; 311 312 this.refreshLock.readLock().lock(); 313 if (this.expires == 0) { 314 needsRefresh = false; 315 } else { 316 long now = System.currentTimeMillis(); 317 long tokenDuration = (now - this.lastRefresh); 318 needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON); 319 } 320 this.refreshLock.readLock().unlock(); 321 322 return needsRefresh; 323 } 324 325 /** 326 * Refresh's this connection's access token using its refresh token. 327 * @throws IllegalStateException if this connection's access token cannot be refreshed. 328 */ 329 public void refresh() { 330 this.refreshLock.writeLock().lock(); 331 332 if (!this.canRefresh()) { 333 this.refreshLock.writeLock().unlock(); 334 throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a " 335 + "refresh token."); 336 } 337 338 URL url = null; 339 try { 340 url = new URL(this.tokenURL); 341 } catch (MalformedURLException e) { 342 this.refreshLock.writeLock().unlock(); 343 assert false : "An invalid refresh URL indicates a bug in the SDK."; 344 throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e); 345 } 346 347 String urlParameters = String.format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", 348 this.refreshToken, this.clientID, this.clientSecret); 349 350 BoxAPIRequest request = new BoxAPIRequest(url, "POST"); 351 request.addHeader("Content-Type", "application/x-www-form-urlencoded"); 352 request.setBody(urlParameters); 353 354 String json; 355 try { 356 BoxJSONResponse response = (BoxJSONResponse) request.send(); 357 json = response.getJSON(); 358 } catch (BoxAPIException e) { 359 this.refreshLock.writeLock().unlock(); 360 throw e; 361 } 362 363 JsonObject jsonObject = JsonObject.readFrom(json); 364 this.accessToken = jsonObject.get("access_token").asString(); 365 this.refreshToken = jsonObject.get("refresh_token").asString(); 366 this.lastRefresh = System.currentTimeMillis(); 367 this.expires = jsonObject.get("expires_in").asLong() * 1000; 368 369 this.notifyRefresh(); 370 371 this.refreshLock.writeLock().unlock(); 372 } 373 374 /** 375 * Notifies refresh event to all the listeners. 376 */ 377 private void notifyRefresh() { 378 for (BoxAPIConnectionListener listener : this.listeners) { 379 listener.onRefresh(); 380 } 381 } 382 383 /** 384 * Add a listener to listen to Box API connection events. 385 * @param listener a listener to listen to Box API connection. 386 */ 387 public void addListener(BoxAPIConnectionListener listener) { 388 this.listeners.add(listener); 389 } 390 391 /** 392 * Remove a listener listening to Box API connection events. 393 * @param listener the listener to remove. 394 */ 395 public void removeListener(BoxAPIConnectionListener listener) { 396 this.listeners.remove(listener); 397 } 398 399 /** 400 * Gets the RequestInterceptor associated with this API connection. 401 * @return the RequestInterceptor associated with this API connection. 402 */ 403 public RequestInterceptor getRequestInterceptor() { 404 return this.interceptor; 405 } 406 407 /** 408 * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API. 409 * @param interceptor the RequestInterceptor. 410 */ 411 public void setRequestInterceptor(RequestInterceptor interceptor) { 412 this.interceptor = interceptor; 413 } 414}