• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

box / box-java-sdk / #4423

19 Feb 2025 11:15AM UTC coverage: 71.967% (+0.04%) from 71.932%
#4423

Pull #1287

github

web-flow
Merge db72c1054 into 29b651987
Pull Request #1287: feat: Support `zstd` encoding

34 of 42 new or added lines in 3 files covered. (80.95%)

3 existing lines in 3 files now uncovered.

8192 of 11383 relevant lines covered (71.97%)

0.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.74
/src/main/java/com/box/sdk/BoxAPIConnection.java
1
package com.box.sdk;
2

3
import static java.lang.String.format;
4
import static java.lang.String.join;
5
import static java.util.Collections.singletonList;
6
import static okhttp3.ConnectionSpec.MODERN_TLS;
7

8
import com.eclipsesource.json.Json;
9
import com.eclipsesource.json.JsonObject;
10
import com.eclipsesource.json.JsonValue;
11
import java.io.IOException;
12
import java.net.MalformedURLException;
13
import java.net.Proxy;
14
import java.net.URI;
15
import java.net.URL;
16
import java.security.KeyManagementException;
17
import java.security.NoSuchAlgorithmException;
18
import java.time.Duration;
19
import java.util.ArrayList;
20
import java.util.HashMap;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Optional;
24
import java.util.concurrent.locks.ReadWriteLock;
25
import java.util.concurrent.locks.ReentrantReadWriteLock;
26
import java.util.regex.Pattern;
27
import javax.net.ssl.HostnameVerifier;
28
import javax.net.ssl.SSLContext;
29
import javax.net.ssl.TrustManager;
30
import javax.net.ssl.X509TrustManager;
31
import okhttp3.Authenticator;
32
import okhttp3.Call;
33
import okhttp3.Credentials;
34
import okhttp3.Headers;
35
import okhttp3.OkHttpClient;
36
import okhttp3.Request;
37
import okhttp3.Response;
38

39
/**
40
 * Represents an authenticated connection to the Box API.
41
 *
42
 * <p>This class handles storing authentication information, automatic token refresh, and rate-limiting. It can also be
43
 * used to configure the Box API endpoint URL in order to hit a different version of the API. Multiple instances of
44
 * BoxAPIConnection may be created to support multi-user login.</p>
45
 */
46
public class BoxAPIConnection {
1✔
47

48
    /**
49
     * Used as a marker to setup connection to use default HostnameVerifier
50
     * Example:<pre>{@code
51
     * BoxApiConnection api = new BoxApiConnection(...);
52
     * HostnameVerifier myHostnameVerifier = ...
53
     * api.configureSslCertificatesValidation(DEFAULT_TRUST_MANAGER, myHostnameVerifier);
54
     * }</pre>
55
     */
56
    public static final X509TrustManager DEFAULT_TRUST_MANAGER = null;
1✔
57
    /**
58
     * Used as a marker to setup connection to use default HostnameVerifier
59
     * Example:<pre>{@code
60
     * BoxApiConnection api = new BoxApiConnection(...);
61
     * X509TrustManager myTrustManager = ...
62
     * api.configureSslCertificatesValidation(myTrustManager, DEFAULT_HOSTNAME_VERIFIER);
63
     * }</pre>
64
     */
65
    public static final HostnameVerifier DEFAULT_HOSTNAME_VERIFIER = null;
1✔
66

67
    /**
68
     * The default maximum number of times an API request will be retried after an error response
69
     * is received.
70
     */
71
    public static final int DEFAULT_MAX_RETRIES = 5;
72
    /**
73
     * Default authorization URL
74
     */
75
    protected static final String DEFAULT_BASE_AUTHORIZATION_URL = "https://account.box.com/api/";
76
    static final String AS_USER_HEADER = "As-User";
77

78
    private static final String API_VERSION = "2.0";
79
    private static final String OAUTH_SUFFIX = "oauth2/authorize";
80
    private static final String TOKEN_URL_SUFFIX = "oauth2/token";
81
    private static final String REVOKE_URL_SUFFIX = "oauth2/revoke";
82
    private static final String DEFAULT_BASE_URL = "https://api.box.com/";
83
    private static final String DEFAULT_BASE_UPLOAD_URL = "https://upload.box.com/api/";
84
    private static final String DEFAULT_BASE_APP_URL = "https://app.box.com";
85

86
    private static final String BOX_NOTIFICATIONS_HEADER = "Box-Notifications";
87

88
    private static final String JAVA_VERSION = System.getProperty("java.version");
1✔
89
    private static final String SDK_VERSION = "4.14.0";
90

91
    /**
92
     * The amount of buffer time, in milliseconds, to use when determining if an access token should be refreshed. For
93
     * example, if REFRESH_EPSILON = 60000 and the access token expires in less than one minute, it will be refreshed.
94
     */
95
    private static final long REFRESH_EPSILON = 60000;
96

97
    private final String clientID;
98
    private final String clientSecret;
99
    private final ReadWriteLock refreshLock;
100
    private X509TrustManager trustManager;
101
    private HostnameVerifier hostnameVerifier;
102

103
    // These volatile fields are used when determining if the access token needs to be refreshed. Since they are used in
104
    // the double-checked lock in getAccessToken(), they must be atomic.
105
    private volatile long lastRefresh;
106
    private volatile long expires;
107

108
    private Proxy proxy;
109
    private String proxyUsername;
110
    private String proxyPassword;
111

112
    private String userAgent;
113
    private String accessToken;
114
    private String refreshToken;
115
    private String tokenURL;
116
    private String revokeURL;
117
    private String baseURL;
118
    private String baseUploadURL;
119
    private String baseAppURL;
120
    private String baseAuthorizationURL;
121
    private boolean autoRefresh;
122
    private int maxRetryAttempts;
123
    private int connectTimeout;
124
    private int readTimeout;
125
    private boolean useZstdCompression;
126
    private final List<BoxAPIConnectionListener> listeners;
127
    private RequestInterceptor interceptor;
128
    private final Map<String, String> customHeaders;
129

130
    private OkHttpClient httpClient;
131
    private OkHttpClient noRedirectsHttpClient;
132
    private Authenticator authenticator;
133

134
    /**
135
     * Constructs a new BoxAPIConnection that authenticates with a developer or access token.
136
     *
137
     * @param accessToken a developer or access token to use for authenticating with the API.
138
     */
139
    public BoxAPIConnection(String accessToken) {
140
        this(null, null, accessToken, null);
1✔
141
    }
1✔
142

143
    /**
144
     * Constructs a new BoxAPIConnection with an access token that can be refreshed.
145
     *
146
     * @param clientID     the client ID to use when refreshing the access token.
147
     * @param clientSecret the client secret to use when refreshing the access token.
148
     * @param accessToken  an initial access token to use for authenticating with the API.
149
     * @param refreshToken an initial refresh token to use when refreshing the access token.
150
     */
151
    public BoxAPIConnection(String clientID, String clientSecret, String accessToken, String refreshToken) {
1✔
152
        this.clientID = clientID;
1✔
153
        this.clientSecret = clientSecret;
1✔
154
        this.accessToken = accessToken;
1✔
155
        this.refreshToken = refreshToken;
1✔
156
        this.baseURL = fixBaseUrl(DEFAULT_BASE_URL);
1✔
157
        this.baseUploadURL = fixBaseUrl(DEFAULT_BASE_UPLOAD_URL);
1✔
158
        this.baseAppURL = DEFAULT_BASE_APP_URL;
1✔
159
        this.baseAuthorizationURL = DEFAULT_BASE_AUTHORIZATION_URL;
1✔
160
        this.autoRefresh = true;
1✔
161
        this.maxRetryAttempts = BoxGlobalSettings.getMaxRetryAttempts();
1✔
162
        this.connectTimeout = BoxGlobalSettings.getConnectTimeout();
1✔
163
        this.readTimeout = BoxGlobalSettings.getReadTimeout();
1✔
164
        this.useZstdCompression = BoxGlobalSettings.getUseZstdCompression();
1✔
165
        this.refreshLock = new ReentrantReadWriteLock();
1✔
166
        this.userAgent = "Box Java SDK v" + SDK_VERSION + " (Java " + JAVA_VERSION + ")";
1✔
167
        this.listeners = new ArrayList<>();
1✔
168
        this.customHeaders = new HashMap<>();
1✔
169

170
        buildHttpClients();
1✔
171
    }
1✔
172

173
    /**
174
     * Constructs a new BoxAPIConnection with an auth code that was obtained from the first half of OAuth.
175
     *
176
     * @param clientID     the client ID to use when exchanging the auth code for an access token.
177
     * @param clientSecret the client secret to use when exchanging the auth code for an access token.
178
     * @param authCode     an auth code obtained from the first half of the OAuth process.
179
     */
180
    public BoxAPIConnection(String clientID, String clientSecret, String authCode) {
181
        this(clientID, clientSecret, null, null);
×
182
        this.authenticate(authCode);
×
183
    }
×
184

185
    /**
186
     * Constructs a new BoxAPIConnection.
187
     *
188
     * @param clientID     the client ID to use when exchanging the auth code for an access token.
189
     * @param clientSecret the client secret to use when exchanging the auth code for an access token.
190
     */
191
    public BoxAPIConnection(String clientID, String clientSecret) {
192
        this(clientID, clientSecret, null, null);
1✔
193
    }
1✔
194

195
    /**
196
     * Constructs a new BoxAPIConnection levaraging BoxConfig.
197
     *
198
     * @param boxConfig BoxConfig file, which should have clientId and clientSecret
199
     */
200
    public BoxAPIConnection(BoxConfig boxConfig) {
201
        this(boxConfig.getClientId(), boxConfig.getClientSecret(), null, null);
×
202
    }
×
203

204
    private void buildHttpClients() {
205
        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
1✔
206
        if (trustManager != null) {
1✔
207
            try {
208
                SSLContext sslContext = SSLContext.getInstance("SSL");
1✔
209
                sslContext.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
1✔
210
                httpClientBuilder.sslSocketFactory(sslContext.getSocketFactory(), trustManager);
1✔
211
            } catch (NoSuchAlgorithmException | KeyManagementException e) {
×
212
                throw new RuntimeException(e);
×
213
            }
1✔
214
        }
215

216
        OkHttpClient.Builder builder = httpClientBuilder
1✔
217
            .followSslRedirects(true)
1✔
218
            .followRedirects(true)
1✔
219
            .connectTimeout(Duration.ofMillis(connectTimeout))
1✔
220
            .readTimeout(Duration.ofMillis(readTimeout))
1✔
221
            .connectionSpecs(singletonList(MODERN_TLS));
1✔
222

223
        if (hostnameVerifier != null) {
1✔
224
            httpClientBuilder.hostnameVerifier(hostnameVerifier);
1✔
225
        }
226

227
        if (proxy != null) {
1✔
228
            builder.proxy(proxy);
1✔
229
            if (proxyUsername != null && proxyPassword != null) {
1✔
230
                builder.proxyAuthenticator((route, response) -> {
1✔
231
                    String credential = Credentials.basic(proxyUsername, proxyPassword);
1✔
232
                    return response.request().newBuilder()
1✔
233
                        .header("Proxy-Authorization", credential)
1✔
234
                        .build();
1✔
235
                });
236
            }
237
            if (this.authenticator != null) {
1✔
238
                builder.proxyAuthenticator(authenticator);
1✔
239
            }
240
        }
241
        builder = modifyHttpClientBuilder(builder);
1✔
242
        if (this.useZstdCompression) {
1✔
243
            builder.addNetworkInterceptor(new ZstdInterceptor());
1✔
244
        }
245

246
        this.httpClient = builder.build();
1✔
247
        this.noRedirectsHttpClient = new OkHttpClient.Builder(httpClient)
1✔
248
            .followSslRedirects(false)
1✔
249
            .followRedirects(false)
1✔
250
            .build();
1✔
251
    }
1✔
252

253
    /**
254
     * Can be used to modify OkHttp.Builder used to create connection. This method is called after all modifications
255
     * were done, thus allowing others to create their own connections and further customize builder.
256
     * @param httpClientBuilder Builder that will be used to create http connection.
257
     * @return Modified builder.
258
     */
259
    protected OkHttpClient.Builder modifyHttpClientBuilder(OkHttpClient.Builder httpClientBuilder) {
260
        return httpClientBuilder;
1✔
261
    }
262

263
    /**
264
     * Sets a proxy authenticator that will be used when proxy requires authentication.
265
     * If you use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)} it adds an authenticator
266
     * that performs Basic authorization. By calling this method you can override this behaviour.
267
     * You do not need to call {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
268
     * in order to set custom authenticator.
269
     *
270
     * @param authenticator Custom authenticator that will be called when proxy asks for authorization.
271
     */
272
    public void setProxyAuthenticator(Authenticator authenticator) {
273
        this.authenticator = authenticator;
1✔
274
        buildHttpClients();
1✔
275
    }
1✔
276

277
    /**
278
     * Restores a BoxAPIConnection from a saved state.
279
     *
280
     * @param clientID     the client ID to use with the connection.
281
     * @param clientSecret the client secret to use with the connection.
282
     * @param state        the saved state that was created with {@link #save}.
283
     * @return a restored API connection.
284
     * @see #save
285
     */
286
    public static BoxAPIConnection restore(String clientID, String clientSecret, String state) {
287
        BoxAPIConnection api = new BoxAPIConnection(clientID, clientSecret);
1✔
288
        api.restore(state);
1✔
289
        return api;
1✔
290
    }
291

292
    /**
293
     * Returns the default authorization URL which is used to perform the authorization_code based OAuth2 flow.
294
     * If custom Authorization URL is needed use instance method {@link BoxAPIConnection#getAuthorizationURL}
295
     *
296
     * @param clientID    the client ID to use with the connection.
297
     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
298
     * @param state       the text string that you choose.
299
     *                    Box sends the same string to your redirect URL when authentication is complete.
300
     * @param scopes      this optional parameter identifies the Box scopes available
301
     *                    to the application once it's authenticated.
302
     * @return the authorization URL
303
     */
304
    public static URL getAuthorizationURL(String clientID, URI redirectUri, String state, List<String> scopes) {
305
        return createFullAuthorizationUrl(DEFAULT_BASE_AUTHORIZATION_URL, clientID, redirectUri, state, scopes);
1✔
306
    }
307

308
    private static URL createFullAuthorizationUrl(
309
        String authorizationUrl, String clientID, URI redirectUri, String state, List<String> scopes
310
    ) {
311
        URLTemplate template = new URLTemplate(authorizationUrl + OAUTH_SUFFIX);
1✔
312
        QueryStringBuilder queryBuilder = new QueryStringBuilder().appendParam("client_id", clientID)
1✔
313
            .appendParam("response_type", "code")
1✔
314
            .appendParam("redirect_uri", redirectUri.toString())
1✔
315
            .appendParam("state", state);
1✔
316

317
        if (scopes != null && !scopes.isEmpty()) {
1✔
318
            queryBuilder.appendParam("scope", join(" ", scopes));
1✔
319
        }
320

321
        return template.buildWithQuery("", queryBuilder.toString());
1✔
322
    }
323

324
    /**
325
     * Authenticates the API connection by obtaining access and refresh tokens using the auth code that was obtained
326
     * from the first half of OAuth.
327
     *
328
     * @param authCode the auth code obtained from the first half of the OAuth process.
329
     */
330
    public void authenticate(String authCode) {
331
        URL url;
332
        try {
333
            url = new URL(this.getTokenURL());
1✔
334
        } catch (MalformedURLException e) {
×
335
            assert false : "An invalid token URL indicates a bug in the SDK.";
×
336
            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
×
337
        }
1✔
338

339
        String urlParameters = format("grant_type=authorization_code&code=%s&client_id=%s&client_secret=%s",
1✔
340
            authCode, this.clientID, this.clientSecret);
341

342
        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1✔
343
        request.shouldAuthenticate(false);
1✔
344
        request.setBody(urlParameters);
1✔
345

346
        // authentication uses form url encoded but response is JSON
347
        try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
1✔
348
            String json = response.getJSON();
1✔
349

350
            JsonObject jsonObject = Json.parse(json).asObject();
1✔
351
            this.accessToken = jsonObject.get("access_token").asString();
1✔
352
            this.refreshToken = jsonObject.get("refresh_token").asString();
1✔
353
            this.lastRefresh = System.currentTimeMillis();
1✔
354
            this.expires = jsonObject.get("expires_in").asLong() * 1000;
1✔
355
        }
356
    }
1✔
357

358
    /**
359
     * Gets the client ID.
360
     *
361
     * @return the client ID.
362
     */
363
    public String getClientID() {
364
        return this.clientID;
1✔
365
    }
366

367
    /**
368
     * Gets the client secret.
369
     *
370
     * @return the client secret.
371
     */
372
    public String getClientSecret() {
373
        return this.clientSecret;
1✔
374
    }
375

376
    /**
377
     * Gets the amount of time for which this connection's access token is valid.
378
     *
379
     * @return the amount of time in milliseconds.
380
     */
381
    public long getExpires() {
382
        return this.expires;
1✔
383
    }
384

385
    /**
386
     * Sets the amount of time for which this connection's access token is valid before it must be refreshed.
387
     *
388
     * @param milliseconds the number of milliseconds for which the access token is valid.
389
     */
390
    public void setExpires(long milliseconds) {
391
        this.expires = milliseconds;
1✔
392
    }
1✔
393

394
    /**
395
     * Gets the token URL that's used to request access tokens.  The default value is
396
     * "https://www.box.com/api/oauth2/token".
397
     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#TOKEN_URL_SUFFIX}.
398
     *
399
     * @return the token URL.
400
     */
401
    public String getTokenURL() {
402
        if (this.tokenURL != null) {
1✔
403
            return this.tokenURL;
1✔
404
        } else {
405
            return this.baseURL + TOKEN_URL_SUFFIX;
1✔
406
        }
407
    }
408

409
    /**
410
     * Returns the URL used for token revocation.
411
     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#REVOKE_URL_SUFFIX}.
412
     *
413
     * @return The url used for token revocation.
414
     */
415
    public String getRevokeURL() {
416
        if (this.revokeURL != null) {
1✔
417
            return this.revokeURL;
1✔
418
        } else {
419
            return this.baseURL + REVOKE_URL_SUFFIX;
1✔
420
        }
421
    }
422

423
    /**
424
     * Gets the base URL that's used when sending requests to the Box API.
425
     * The URL is created from {@link BoxAPIConnection#baseURL} and {@link BoxAPIConnection#API_VERSION}.
426
     * The default value is "https://api.box.com/2.0/".
427
     *
428
     * @return the base URL.
429
     */
430
    public String getBaseURL() {
431
        return this.baseURL + API_VERSION + "/";
1✔
432
    }
433

434
    /**
435
     * Sets the base URL to be used when sending requests to the Box API. For example, the default base URL is
436
     * "https://api.box.com/". This method changes how {@link BoxAPIConnection#getRevokeURL()}
437
     * and {@link BoxAPIConnection#getTokenURL()} are constructed.
438
     *
439
     * @param baseURL a base URL
440
     */
441
    public void setBaseURL(String baseURL) {
442
        this.baseURL = fixBaseUrl(baseURL);
1✔
443
    }
1✔
444

445
    /**
446
     * Gets the base upload URL that's used when performing file uploads to Box.
447
     * The URL is created from {@link BoxAPIConnection#baseUploadURL} and {@link BoxAPIConnection#API_VERSION}.
448
     *
449
     * @return the base upload URL.
450
     */
451
    public String getBaseUploadURL() {
452
        return this.baseUploadURL + API_VERSION + "/";
1✔
453
    }
454

455
    /**
456
     * Sets the base upload URL to be used when performing file uploads to Box.
457
     *
458
     * @param baseUploadURL a base upload URL.
459
     */
460
    public void setBaseUploadURL(String baseUploadURL) {
461
        this.baseUploadURL = fixBaseUrl(baseUploadURL);
1✔
462
    }
1✔
463

464
    /**
465
     * Returns the authorization URL which is used to perform the authorization_code based OAuth2 flow.
466
     * The URL is created from {@link BoxAPIConnection#baseAuthorizationURL} and {@link BoxAPIConnection#OAUTH_SUFFIX}.
467
     *
468
     * @param redirectUri the URL to which Box redirects the browser when authentication completes.
469
     * @param state       the text string that you choose.
470
     *                    Box sends the same string to your redirect URL when authentication is complete.
471
     * @param scopes      this optional parameter identifies the Box scopes available
472
     *                    to the application once it's authenticated.
473
     * @return the authorization URL
474
     */
475
    public URL getAuthorizationURL(URI redirectUri, String state, List<String> scopes) {
476
        return createFullAuthorizationUrl(this.baseAuthorizationURL, this.clientID, redirectUri, state, scopes);
1✔
477
    }
478

479
    /**
480
     * Sets authorization base URL which is used to perform the authorization_code based OAuth2 flow.
481
     *
482
     * @param baseAuthorizationURL Authorization URL. Default value is https://account.box.com/api/.
483
     */
484
    public void setBaseAuthorizationURL(String baseAuthorizationURL) {
485
        this.baseAuthorizationURL = fixBaseUrl(baseAuthorizationURL);
1✔
486
    }
1✔
487

488
    /**
489
     * Gets the user agent that's used when sending requests to the Box API.
490
     *
491
     * @return the user agent.
492
     */
493
    public String getUserAgent() {
494
        return this.userAgent;
1✔
495
    }
496

497
    /**
498
     * Sets the user agent to be used when sending requests to the Box API.
499
     *
500
     * @param userAgent the user agent.
501
     */
502
    public void setUserAgent(String userAgent) {
503
        this.userAgent = userAgent;
×
504
    }
×
505

506
    /**
507
     * Gets the base App url. Used for e.g. file requests.
508
     *
509
     * @return the base App Url.
510
     */
511
    public String getBaseAppUrl() {
512
        return this.baseAppURL;
1✔
513
    }
514

515
    /**
516
     * Sets the base App url. Used for e.g. file requests.
517
     *
518
     * @param baseAppURL a base App Url.
519
     */
520
    public void setBaseAppUrl(String baseAppURL) {
521
        this.baseAppURL = baseAppURL;
1✔
522
    }
1✔
523

524
    /**
525
     * Gets an access token that can be used to authenticate an API request. This method will automatically refresh the
526
     * access token if it has expired since the last call to <code>getAccessToken()</code>.
527
     *
528
     * @return a valid access token that can be used to authenticate an API request.
529
     */
530
    public String getAccessToken() {
531
        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
1✔
532
            this.refreshLock.writeLock().lock();
1✔
533
            try {
534
                if (this.needsRefresh()) {
1✔
535
                    this.refresh();
1✔
536
                }
537
            } finally {
538
                this.refreshLock.writeLock().unlock();
1✔
539
            }
540
        }
541

542
        this.refreshLock.readLock().lock();
1✔
543
        try {
544
            return this.accessToken;
1✔
545
        } finally {
546
            this.refreshLock.readLock().unlock();
1✔
547
        }
548
    }
549

550
    /**
551
     * Sets the access token to use when authenticating API requests.
552
     *
553
     * @param accessToken a valid access token to use when authenticating API requests.
554
     */
555
    public void setAccessToken(String accessToken) {
556
        this.accessToken = accessToken;
1✔
557
    }
1✔
558

559
    /**
560
     * Gets the refresh lock to be used when refreshing an access token.
561
     *
562
     * @return the refresh lock.
563
     */
564
    protected ReadWriteLock getRefreshLock() {
565
        return this.refreshLock;
×
566
    }
567

568
    /**
569
     * Gets a refresh token that can be used to refresh an access token.
570
     *
571
     * @return a valid refresh token.
572
     */
573
    public String getRefreshToken() {
574
        return this.refreshToken;
1✔
575
    }
576

577
    /**
578
     * Sets the refresh token to use when refreshing an access token.
579
     *
580
     * @param refreshToken a valid refresh token.
581
     */
582
    public void setRefreshToken(String refreshToken) {
583
        this.refreshToken = refreshToken;
×
584
    }
×
585

586
    /**
587
     * Gets the last time that the access token was refreshed.
588
     *
589
     * @return the last refresh time in milliseconds.
590
     */
591
    public long getLastRefresh() {
592
        return this.lastRefresh;
1✔
593
    }
594

595
    /**
596
     * Sets the last time that the access token was refreshed.
597
     *
598
     * <p>This value is used when determining if an access token needs to be auto-refreshed. If the amount of time since
599
     * the last refresh exceeds the access token's expiration time, then the access token will be refreshed.</p>
600
     *
601
     * @param lastRefresh the new last refresh time in milliseconds.
602
     */
603
    public void setLastRefresh(long lastRefresh) {
604
        this.lastRefresh = lastRefresh;
1✔
605
    }
1✔
606

607
    /**
608
     * Gets whether or not automatic refreshing of this connection's access token is enabled. Defaults to true.
609
     *
610
     * @return true if auto token refresh is enabled; otherwise false.
611
     */
612
    public boolean getAutoRefresh() {
613
        return this.autoRefresh;
1✔
614
    }
615

616
    /**
617
     * Enables or disables automatic refreshing of this connection's access token. Defaults to true.
618
     *
619
     * @param autoRefresh true to enable auto token refresh; otherwise false.
620
     */
621
    public void setAutoRefresh(boolean autoRefresh) {
622
        this.autoRefresh = autoRefresh;
1✔
623
    }
1✔
624

625

626
    /**
627
     * Gets the maximum number of times an API request will be retried after an error response
628
     * is received.
629
     *
630
     * @return the maximum number of request attempts.
631
     */
632
    public int getMaxRetryAttempts() {
633
        return this.maxRetryAttempts;
1✔
634
    }
635

636
    /**
637
     * Sets the maximum number of times an API request will be retried after an error response
638
     * is received.
639
     *
640
     * @param attempts the maximum number of request attempts.
641
     */
642
    public void setMaxRetryAttempts(int attempts) {
643
        this.maxRetryAttempts = attempts;
1✔
644
    }
1✔
645

646
    /**
647
     * Gets the connect timeout for this connection in milliseconds.
648
     *
649
     * @return the number of milliseconds to connect before timing out.
650
     */
651
    public int getConnectTimeout() {
652
        return this.connectTimeout;
1✔
653
    }
654

655
    /**
656
     * Sets the connect timeout for this connection.
657
     *
658
     * @param connectTimeout The number of milliseconds to wait for the connection to be established.
659
     */
660
    public void setConnectTimeout(int connectTimeout) {
661
        this.connectTimeout = connectTimeout;
1✔
662
        buildHttpClients();
1✔
663
    }
1✔
664

665
    /*
666
     * Gets if request use zstd encoding when possible
667
     * @return true if request use zstd encoding when possible
668
     */
669
    public boolean getUseZstdCompression() {
NEW
670
        return this.useZstdCompression;
×
671
    }
672

673
    /*
674
     * Sets if request use zstd encoding when possible
675
     * @param useZstdCompression true if request use zstd encoding when possible
676
     */
677
    public void setUseZstdCompression(boolean useZstdCompression) {
NEW
678
        this.useZstdCompression = useZstdCompression;
×
NEW
679
        buildHttpClients();
×
NEW
680
    }
×
681

682
    /**
683
     * Gets the read timeout for this connection in milliseconds.
684
     *
685
     * @return the number of milliseconds to wait for bytes to be read before timing out.
686
     */
687
    public int getReadTimeout() {
688
        return this.readTimeout;
1✔
689
    }
690

691
    /**
692
     * Sets the read timeout for this connection.
693
     *
694
     * @param readTimeout The number of milliseconds to wait for bytes to be read.
695
     */
696
    public void setReadTimeout(int readTimeout) {
697
        this.readTimeout = readTimeout;
1✔
698
        buildHttpClients();
1✔
699
    }
1✔
700

701
    /**
702
     * Gets the proxy value to use for API calls to Box.
703
     *
704
     * @return the current proxy.
705
     */
706
    public Proxy getProxy() {
707
        return this.proxy;
×
708
    }
709

710
    /**
711
     * Sets the proxy to use for API calls to Box.
712
     *
713
     * @param proxy the proxy to use for API calls to Box.
714
     */
715
    public void setProxy(Proxy proxy) {
716
        this.proxy = proxy;
1✔
717
        buildHttpClients();
1✔
718
    }
1✔
719

720
    /**
721
     * Gets the username to use for a proxy that requires basic auth.
722
     *
723
     * @return the username to use for a proxy that requires basic auth.
724
     */
725
    public String getProxyUsername() {
726
        return this.proxyUsername;
×
727
    }
728

729
    /**
730
     * Sets the username to use for a proxy that requires basic auth.
731
     *
732
     * @param proxyUsername the username to use for a proxy that requires basic auth.
733
     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
734
     */
735
    public void setProxyUsername(String proxyUsername) {
736
        this.proxyUsername = proxyUsername;
×
737
        buildHttpClients();
×
738
    }
×
739

740
    /**
741
     * Gets the password to use for a proxy that requires basic auth.
742
     *
743
     * @return the password to use for a proxy that requires basic auth.
744
     */
745
    public String getProxyPassword() {
746
        return this.proxyPassword;
×
747
    }
748

749
    /**
750
     * Sets the proxy user and password used in basic authentication
751
     *
752
     * @param proxyUsername Username to use for a proxy that requires basic auth.
753
     * @param proxyPassword Password to use for a proxy that requires basic auth.
754
     */
755
    public void setProxyBasicAuthentication(String proxyUsername, String proxyPassword) {
756
        this.proxyUsername = proxyUsername;
1✔
757
        this.proxyPassword = proxyPassword;
1✔
758
        buildHttpClients();
1✔
759
    }
1✔
760

761
    /**
762
     * Sets the password to use for a proxy that requires basic auth.
763
     *
764
     * @param proxyPassword the password to use for a proxy that requires basic auth.
765
     * @deprecated Use {@link BoxAPIConnection#setProxyBasicAuthentication(String, String)}
766
     */
767
    public void setProxyPassword(String proxyPassword) {
768
        this.proxyPassword = proxyPassword;
×
769
        buildHttpClients();
×
770
    }
×
771

772
    /**
773
     * Determines if this connection's access token can be refreshed. An access token cannot be refreshed if a refresh
774
     * token was never set.
775
     *
776
     * @return true if the access token can be refreshed; otherwise false.
777
     */
778
    public boolean canRefresh() {
779
        return this.refreshToken != null;
1✔
780
    }
781

782
    /**
783
     * Determines if this connection's access token has expired and needs to be refreshed.
784
     *
785
     * @return true if the access token needs to be refreshed; otherwise false.
786
     */
787
    public boolean needsRefresh() {
788
        boolean needsRefresh;
789

790
        this.refreshLock.readLock().lock();
1✔
791
        try {
792
            long now = System.currentTimeMillis();
1✔
793
            long tokenDuration = (now - this.lastRefresh);
1✔
794
            needsRefresh = (tokenDuration >= this.expires - REFRESH_EPSILON);
1✔
795
        } finally {
796
            this.refreshLock.readLock().unlock();
1✔
797
        }
798

799
        return needsRefresh;
1✔
800
    }
801

802
    /**
803
     * Refresh's this connection's access token using its refresh token.
804
     *
805
     * @throws IllegalStateException if this connection's access token cannot be refreshed.
806
     */
807
    public void refresh() {
808
        this.refreshLock.writeLock().lock();
1✔
809
        try {
810
            if (!this.canRefresh()) {
1✔
811
                throw new IllegalStateException("The BoxAPIConnection cannot be refreshed because it doesn't have a "
×
812
                        + "refresh token.");
813
            }
814

815
            URL url;
816
            try {
817
                url = new URL(getTokenURL());
1✔
818
            } catch (MalformedURLException e) {
×
819
                assert false : "An invalid refresh URL indicates a bug in the SDK.";
×
820
                throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
×
821
            }
1✔
822

823
            BoxAPIRequest request = createTokenRequest(url);
1✔
824

825
            String json;
826
            try (BoxAPIResponse boxAPIResponse = request.send()) {
1✔
827
                BoxJSONResponse response = (BoxJSONResponse) boxAPIResponse;
1✔
828
                json = response.getJSON();
1✔
829
            } catch (BoxAPIException e) {
×
830
                this.notifyError(e);
×
831
                throw e;
×
832
            }
1✔
833

834
            extractTokens(Json.parse(json).asObject());
1✔
835
            this.notifyRefresh();
1✔
836
        } finally {
837
            this.refreshLock.writeLock().unlock();
1✔
838
        }
839
    }
1✔
840

841
    /**
842
     * Restores a saved connection state into this BoxAPIConnection.
843
     *
844
     * @param state the saved state that was created with {@link #save}.
845
     * @see #save
846
     */
847
    public void restore(String state) {
848
        JsonObject json = Json.parse(state).asObject();
1✔
849
        String accessToken = json.get("accessToken").asString();
1✔
850
        String refreshToken = getKeyValueOrDefault(json, "refreshToken", null);
1✔
851
        long lastRefresh = json.get("lastRefresh").asLong();
1✔
852
        long expires = json.get("expires").asLong();
1✔
853
        String userAgent = json.get("userAgent").asString();
1✔
854
        String tokenURL = getKeyValueOrDefault(json, "tokenURL", null);
1✔
855
        String revokeURL = getKeyValueOrDefault(json, "revokeURL", null);
1✔
856
        String baseURL = adoptBaseUrlWhenLoadingFromOldVersion(
1✔
857
            getKeyValueOrDefault(json, "baseURL", DEFAULT_BASE_URL)
1✔
858
        );
859
        String baseUploadURL = adoptUploadBaseUrlWhenLoadingFromOldVersion(
1✔
860
            getKeyValueOrDefault(json, "baseUploadURL", DEFAULT_BASE_UPLOAD_URL)
1✔
861
        );
862
        String authorizationURL =
1✔
863
            getKeyValueOrDefault(json, "authorizationURL", DEFAULT_BASE_AUTHORIZATION_URL);
1✔
864
        boolean autoRefresh = json.get("autoRefresh").asBoolean();
1✔
865

866
        // Try to read deprecated value
867
        int maxRequestAttempts = -1;
1✔
868
        if (json.names().contains("maxRequestAttempts")) {
1✔
869
            maxRequestAttempts = json.get("maxRequestAttempts").asInt();
1✔
870
        }
871

872
        int maxRetryAttempts = -1;
1✔
873
        if (json.names().contains("maxRetryAttempts")) {
1✔
874
            maxRetryAttempts = json.get("maxRetryAttempts").asInt();
1✔
875
        }
876

877
        this.accessToken = accessToken;
1✔
878
        this.refreshToken = refreshToken;
1✔
879
        this.lastRefresh = lastRefresh;
1✔
880
        this.expires = expires;
1✔
881
        this.userAgent = userAgent;
1✔
882
        this.tokenURL = tokenURL;
1✔
883
        this.revokeURL = revokeURL;
1✔
884
        this.setBaseURL(baseURL);
1✔
885
        this.setBaseUploadURL(baseUploadURL);
1✔
886
        this.setBaseAuthorizationURL(authorizationURL);
1✔
887
        this.autoRefresh = autoRefresh;
1✔
888

889
        // Try to use deprecated value "maxRequestAttempts", else use newer value "maxRetryAttempts"
890
        if (maxRequestAttempts > -1) {
1✔
891
            this.maxRetryAttempts = maxRequestAttempts - 1;
1✔
892
        }
893
        if (maxRetryAttempts > -1) {
1✔
894
            this.maxRetryAttempts = maxRetryAttempts;
1✔
895
        }
896

897
    }
1✔
898

899
    private String adoptBaseUrlWhenLoadingFromOldVersion(String url) {
900
        if (url == null) {
1✔
901
            return null;
×
902
        }
903
        String urlEndingWithSlash = fixBaseUrl(url);
1✔
904
        return urlEndingWithSlash.equals("https://api.box.com/2.0/")
1✔
905
            ? DEFAULT_BASE_URL
1✔
906
            : urlEndingWithSlash;
1✔
907
    }
908

909
    private String adoptUploadBaseUrlWhenLoadingFromOldVersion(String url) {
910
        if (url == null) {
1✔
911
            return null;
×
912
        }
913
        String urlEndingWithSlash = fixBaseUrl(url);
1✔
914
        return urlEndingWithSlash.equals("https://upload.box.com/api/2.0/")
1✔
915
            ? DEFAULT_BASE_UPLOAD_URL
1✔
916
            : urlEndingWithSlash;
1✔
917
    }
918

919
    protected String getKeyValueOrDefault(JsonObject json, String key, String defaultValue) {
920
        return Optional.ofNullable(json.get(key))
1✔
921
            .filter(js -> !js.isNull())
1✔
922
            .map(JsonValue::asString)
1✔
923
            .orElse(defaultValue);
1✔
924
    }
925

926
    /**
927
     * Notifies a refresh event to all the listeners.
928
     */
929
    protected void notifyRefresh() {
930
        for (BoxAPIConnectionListener listener : this.listeners) {
1✔
931
            listener.onRefresh(this);
×
932
        }
×
933
    }
1✔
934

935
    /**
936
     * Notifies an error event to all the listeners.
937
     *
938
     * @param error A BoxAPIException instance.
939
     */
940
    protected void notifyError(BoxAPIException error) {
941
        for (BoxAPIConnectionListener listener : this.listeners) {
×
942
            listener.onError(this, error);
×
943
        }
×
944
    }
×
945

946
    /**
947
     * Add a listener to listen to Box API connection events.
948
     *
949
     * @param listener a listener to listen to Box API connection.
950
     */
951
    public void addListener(BoxAPIConnectionListener listener) {
952
        this.listeners.add(listener);
×
953
    }
×
954

955
    /**
956
     * Remove a listener listening to Box API connection events.
957
     *
958
     * @param listener the listener to remove.
959
     */
960
    public void removeListener(BoxAPIConnectionListener listener) {
961
        this.listeners.remove(listener);
×
962
    }
×
963

964
    /**
965
     * Gets the RequestInterceptor associated with this API connection.
966
     *
967
     * @return the RequestInterceptor associated with this API connection.
968
     */
969
    public RequestInterceptor getRequestInterceptor() {
970
        return this.interceptor;
1✔
971
    }
972

973
    /**
974
     * Sets a RequestInterceptor that can intercept requests and manipulate them before they're sent to the Box API.
975
     *
976
     * @param interceptor the RequestInterceptor.
977
     */
978
    public void setRequestInterceptor(RequestInterceptor interceptor) {
979
        this.interceptor = interceptor;
1✔
980
    }
1✔
981

982
    /**
983
     * Get a lower-scoped token restricted to a resource for the list of scopes that are passed.
984
     *
985
     * @param scopes   the list of scopes to which the new token should be restricted for
986
     * @param resource the resource for which the new token has to be obtained
987
     * @return scopedToken which has access token and other details
988
     * @throws BoxAPIException if resource is not a valid Box API endpoint or shared link
989
     */
990
    public ScopedToken getLowerScopedToken(List<String> scopes, String resource) {
991
        assert (scopes != null);
1✔
992
        assert (scopes.size() > 0);
1✔
993
        URL url;
994
        try {
995
            url = new URL(this.getTokenURL());
1✔
996
        } catch (MalformedURLException e) {
×
997
            assert false : "An invalid refresh URL indicates a bug in the SDK.";
×
998
            throw new BoxAPIException("An invalid refresh URL indicates a bug in the SDK.", e);
×
999
        }
1✔
1000

1001
        StringBuilder spaceSeparatedScopes = this.buildScopesForTokenDownscoping(scopes);
1✔
1002

1003
        String urlParameters = format("grant_type=urn:ietf:params:oauth:grant-type:token-exchange"
1✔
1004
                + "&subject_token_type=urn:ietf:params:oauth:token-type:access_token&subject_token=%s"
1005
                + "&scope=%s",
1006
            this.getAccessToken(), spaceSeparatedScopes);
1✔
1007

1008
        if (resource != null) {
1✔
1009

1010
            ResourceLinkType resourceType = this.determineResourceLinkType(resource);
×
1011

1012
            if (resourceType == ResourceLinkType.APIEndpoint) {
×
1013
                urlParameters = format(urlParameters + "&resource=%s", resource);
×
1014
            } else if (resourceType == ResourceLinkType.SharedLink) {
×
1015
                urlParameters = format(urlParameters + "&box_shared_link=%s", resource);
×
1016
            } else if (resourceType == ResourceLinkType.Unknown) {
×
1017
                String argExceptionMessage = format("Unable to determine resource type: %s", resource);
×
1018
                BoxAPIException e = new BoxAPIException(argExceptionMessage);
×
1019
                this.notifyError(e);
×
1020
                throw e;
×
1021
            } else {
1022
                String argExceptionMessage = format("Unhandled resource type: %s", resource);
×
1023
                BoxAPIException e = new BoxAPIException(argExceptionMessage);
×
1024
                this.notifyError(e);
×
1025
                throw e;
×
1026
            }
1027
        }
1028

1029
        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1✔
1030
        request.shouldAuthenticate(false);
1✔
1031
        request.setBody(urlParameters);
1✔
1032

1033
        String jsonResponse;
1034
        try (BoxJSONResponse response = (BoxJSONResponse) request.send()) {
×
1035
            jsonResponse = response.getJSON();
×
1036
        } catch (BoxAPIException e) {
×
1037
            this.notifyError(e);
×
1038
            throw e;
×
1039
        }
×
1040

1041
        JsonObject jsonObject = Json.parse(jsonResponse).asObject();
×
1042
        ScopedToken token = new ScopedToken(jsonObject);
×
1043
        token.setObtainedAt(System.currentTimeMillis());
×
1044
        token.setExpiresIn(jsonObject.get("expires_in").asLong() * 1000);
×
1045
        return token;
×
1046
    }
1047

1048
    /**
1049
     * Convert List<String> to space-delimited String.
1050
     * Needed for versions prior to Java 8, which don't have String.join(delimiter, list)
1051
     *
1052
     * @param scopes the list of scopes to read from
1053
     * @return space-delimited String of scopes
1054
     */
1055
    private StringBuilder buildScopesForTokenDownscoping(List<String> scopes) {
1056
        StringBuilder spaceSeparatedScopes = new StringBuilder();
1✔
1057
        for (int i = 0; i < scopes.size(); i++) {
1✔
1058
            spaceSeparatedScopes.append(scopes.get(i));
1✔
1059
            if (i < scopes.size() - 1) {
1✔
1060
                spaceSeparatedScopes.append(" ");
×
1061
            }
1062
        }
1063

1064
        return spaceSeparatedScopes;
1✔
1065
    }
1066

1067
    /**
1068
     * Determines the type of resource, given a link to a Box resource.
1069
     *
1070
     * @param resourceLink the resource URL to check
1071
     * @return ResourceLinkType that categorizes the provided resourceLink
1072
     */
1073
    protected ResourceLinkType determineResourceLinkType(String resourceLink) {
1074

1075
        ResourceLinkType resourceType = ResourceLinkType.Unknown;
1✔
1076

1077
        try {
1078
            URL validUrl = new URL(resourceLink);
1✔
1079
            String validURLStr = validUrl.toString();
1✔
1080
            final String apiFilesEndpointPattern = ".*box.com/2.0/files/\\d+";
1✔
1081
            final String apiFoldersEndpointPattern = ".*box.com/2.0/folders/\\d+";
1✔
1082
            final String sharedLinkPattern = "(.*box.com/s/.*|.*box.com.*s=.*)";
1✔
1083

1084
            if (Pattern.matches(apiFilesEndpointPattern, validURLStr)
1✔
1085
                || Pattern.matches(apiFoldersEndpointPattern, validURLStr)) {
1✔
1086
                resourceType = ResourceLinkType.APIEndpoint;
1✔
1087
            } else if (Pattern.matches(sharedLinkPattern, validURLStr)) {
1✔
1088
                resourceType = ResourceLinkType.SharedLink;
1✔
1089
            }
1090
        } catch (MalformedURLException e) {
1✔
1091
            //Swallow exception and return default ResourceLinkType set at top of function
1092
        }
1✔
1093

1094
        return resourceType;
1✔
1095
    }
1096

1097
    /**
1098
     * Revokes the tokens associated with this API connection.  This results in the connection no
1099
     * longer being able to make API calls until a fresh authorization is made by calling authenticate()
1100
     */
1101
    public void revokeToken() {
1102

1103
        URL url;
1104
        try {
1105
            url = new URL(getRevokeURL());
1✔
1106
        } catch (MalformedURLException e) {
×
1107
            assert false : "An invalid refresh URL indicates a bug in the SDK.";
×
1108
            throw new RuntimeException("An invalid refresh URL indicates a bug in the SDK.", e);
×
1109
        }
1✔
1110

1111
        String urlParameters = format("token=%s&client_id=%s&client_secret=%s",
1✔
1112
            this.accessToken, this.clientID, this.clientSecret);
1113

1114
        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1✔
1115
        request.shouldAuthenticate(false);
1✔
1116
        request.setBody(urlParameters);
1✔
1117

1118
        request.send().close();
1✔
1119
    }
1✔
1120

1121
    /**
1122
     * Saves the state of this connection to a string so that it can be persisted and restored at a later time.
1123
     *
1124
     * <p>Note that proxy settings aren't automatically saved or restored. This is mainly due to security concerns
1125
     * around persisting proxy authentication details to the state string. If your connection uses a proxy, you will
1126
     * have to manually configure it again after restoring the connection.</p>
1127
     *
1128
     * @return the state of this connection.
1129
     * @see #restore
1130
     */
1131
    public String save() {
1132
        JsonObject state = new JsonObject()
1✔
1133
            .add("accessToken", this.accessToken)
1✔
1134
            .add("refreshToken", this.refreshToken)
1✔
1135
            .add("lastRefresh", this.lastRefresh)
1✔
1136
            .add("expires", this.expires)
1✔
1137
            .add("userAgent", this.userAgent)
1✔
1138
            .add("tokenURL", this.tokenURL)
1✔
1139
            .add("revokeURL", this.revokeURL)
1✔
1140
            .add("baseURL", this.baseURL)
1✔
1141
            .add("baseUploadURL", this.baseUploadURL)
1✔
1142
            .add("authorizationURL", this.baseAuthorizationURL)
1✔
1143
            .add("autoRefresh", this.autoRefresh)
1✔
1144
            .add("maxRetryAttempts", this.maxRetryAttempts);
1✔
1145
        return state.toString();
1✔
1146
    }
1147

1148
    String lockAccessToken() {
1149
        if (this.autoRefresh && this.canRefresh() && this.needsRefresh()) {
1✔
1150
            this.refreshLock.writeLock().lock();
×
1151
            try {
1152
                if (this.needsRefresh()) {
×
1153
                    this.refresh();
×
1154
                }
1155
                this.refreshLock.readLock().lock();
×
1156
            } finally {
1157
                this.refreshLock.writeLock().unlock();
×
1158
            }
×
1159
        } else {
1160
            this.refreshLock.readLock().lock();
1✔
1161
        }
1162

1163
        return this.accessToken;
1✔
1164
    }
1165

1166
    void unlockAccessToken() {
1167
        this.refreshLock.readLock().unlock();
1✔
1168
    }
1✔
1169

1170
    /**
1171
     * Get the value for the X-Box-UA header.
1172
     *
1173
     * @return the header value.
1174
     */
1175
    String getBoxUAHeader() {
1176

1177
        return "agent=box-java-sdk/" + SDK_VERSION + "; env=Java/" + JAVA_VERSION;
1✔
1178
    }
1179

1180
    /**
1181
     * Sets a custom header to be sent on all requests through this API connection.
1182
     *
1183
     * @param header the header name.
1184
     * @param value  the header value.
1185
     */
1186
    public void setCustomHeader(String header, String value) {
1187
        this.customHeaders.put(header, value);
1✔
1188
    }
1✔
1189

1190
    /**
1191
     * Removes a custom header, so it will no longer be sent on requests through this API connection.
1192
     *
1193
     * @param header the header name.
1194
     */
1195
    public void removeCustomHeader(String header) {
1196
        this.customHeaders.remove(header);
1✔
1197
    }
1✔
1198

1199
    /**
1200
     * Suppresses email notifications from API actions.  This is typically used by security or admin applications
1201
     * to prevent spamming end users when doing automated processing on their content.
1202
     */
1203
    public void suppressNotifications() {
1204
        this.setCustomHeader(BOX_NOTIFICATIONS_HEADER, "off");
1✔
1205
    }
1✔
1206

1207
    /**
1208
     * Re-enable email notifications from API actions if they have been suppressed.
1209
     *
1210
     * @see #suppressNotifications
1211
     */
1212
    public void enableNotifications() {
1213
        this.removeCustomHeader(BOX_NOTIFICATIONS_HEADER);
1✔
1214
    }
1✔
1215

1216
    /**
1217
     * Set this API connection to make API calls on behalf of another users, impersonating them.  This
1218
     * functionality can only be used by admins and service accounts.
1219
     *
1220
     * @param userID the ID of the user to act as.
1221
     */
1222
    public void asUser(String userID) {
1223
        this.setCustomHeader(AS_USER_HEADER, userID);
1✔
1224
    }
1✔
1225

1226
    /**
1227
     * Sets this API connection to make API calls on behalf of the user with whom the access token is associated.
1228
     * This undoes any previous calls to asUser().
1229
     *
1230
     * @see #asUser
1231
     */
1232
    public void asSelf() {
1233
        this.removeCustomHeader(AS_USER_HEADER);
1✔
1234
    }
1✔
1235

1236
    /**
1237
     * Used to override default SSL certification handling. For example, you can provide your own
1238
     * trust manager or hostname verifier to allow self-signed certificates.
1239
     * You can check examples <a href="https://github.com/box/box-java-sdk/blob/main/doc/configuration.md#ssl-configuration">here</a>.
1240
     *
1241
     * @param trustManager     TrustManager that verifies certificates are valid.
1242
     * @param hostnameVerifier HostnameVerifier that allows you to specify what hostnames are allowed.
1243
     */
1244
    public void configureSslCertificatesValidation(X509TrustManager trustManager, HostnameVerifier hostnameVerifier) {
1245
        this.trustManager = trustManager;
1✔
1246
        this.hostnameVerifier = hostnameVerifier;
1✔
1247
        buildHttpClients();
1✔
1248
    }
1✔
1249

1250
    Map<String, String> getHeaders() {
1251
        return this.customHeaders;
1✔
1252
    }
1253

1254
    protected void extractTokens(JsonObject jsonObject) {
1255
        this.accessToken = jsonObject.get("access_token").asString();
1✔
1256
        this.refreshToken = jsonObject.get("refresh_token").asString();
1✔
1257
        this.lastRefresh = System.currentTimeMillis();
1✔
1258
        this.expires = jsonObject.get("expires_in").asLong() * 1000;
1✔
1259
    }
1✔
1260

1261
    protected BoxAPIRequest createTokenRequest(URL url) {
1262
        String urlParameters = format("grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s",
1✔
1263
            this.refreshToken, this.clientID, this.clientSecret);
1264

1265
        BoxAPIRequest request = new BoxAPIRequest(this, url, "POST");
1✔
1266
        request.shouldAuthenticate(false);
1✔
1267
        request.setBody(urlParameters);
1✔
1268
        return request;
1✔
1269
    }
1270

1271
    private String fixBaseUrl(String baseUrl) {
1272
        return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
1✔
1273
    }
1274

1275
    Response execute(Request request) {
1276
        return executeOnClient(httpClient, request);
1✔
1277
    }
1278

1279
    Response executeWithoutRedirect(Request request) {
1280
        return executeOnClient(noRedirectsHttpClient, request);
×
1281
    }
1282

1283
    protected Call createNewCall(OkHttpClient httpClient, Request request) {
1284
        return httpClient.newCall(request);
1✔
1285
    }
1286

1287
    private Response executeOnClient(OkHttpClient httpClient, Request request) {
1288
        try {
1289
            return createNewCall(httpClient, request).execute();
1✔
1290
        } catch (IOException e) {
1✔
1291
            throw new BoxAPIException("Couldn't connect to the Box API due to a network error. Request\n"
1✔
1292
                + toSanitizedRequest(request), e);
1✔
1293
        }
1294
    }
1295

1296
    protected X509TrustManager getTrustManager() {
1297
        return trustManager;
×
1298
    }
1299

1300
    protected HostnameVerifier getHostnameVerifier() {
1301
        return hostnameVerifier;
×
1302
    }
1303

1304
    /**
1305
     * Used to categorize the types of resource links.
1306
     */
1307
    protected enum ResourceLinkType {
1✔
1308
        /**
1309
         * Catch-all default for resource links that are unknown.
1310
         */
1311
        Unknown,
1✔
1312

1313
        /**
1314
         * Resource URLs that point to an API endipoint such as https://api.box.com/2.0/files/:file_id.
1315
         */
1316
        APIEndpoint,
1✔
1317

1318
        /**
1319
         * Resource URLs that point to a resource that has been shared
1320
         * such as https://example.box.com/s/qwertyuiop1234567890asdfghjk
1321
         * or https://example.app.box.com/notes/0987654321?s=zxcvbnm1234567890asdfghjk.
1322
         */
1323
        SharedLink
1✔
1324
    }
1325

1326
    private Request toSanitizedRequest(Request originalRequest) {
1327
        Headers sanitizedHeaders = BoxSensitiveDataSanitizer.sanitizeHeaders(originalRequest.headers());
1✔
1328

1329
        return originalRequest.newBuilder()
1✔
1330
            .headers(sanitizedHeaders)
1✔
1331
            .build();
1✔
1332
    }
1333
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc