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}