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

smartsheet / smartsheet-java-sdk / #44

25 Aug 2023 05:39PM UTC coverage: 50.55% (+0.1%) from 50.427%
#44

push

github-actions

web-flow
Fix remaining Checkstyle violations and Enable Checkstyle (#58)

* Fix remaining Checkstyle violations and Enable Checkstyle

We are now down to `20` checkstyle violations in main and `0` violations in test.

The remaining 20 violations are not trivial to fix, so I've set checkstyle to allow those 20 violations to exist, but to fail the build if we ever exceed 20 violations. This should make the build fail if any new violations are added.

For tests, we do not allow _any_ violations. This means adding a single violation will fail the build. Once the 20 violations in main are cleaned up, we can make main and test have the same config.

Note: This MR also changes our PR pipeline to run `./gradlew clean build` instead of `./gradlew clean test`. The reason for this is that build runs all the tests and performs all the other checks (such as checkstyle), whereas `test` didn't run checkstyle and we wouldn't have noticed violations until we tried to deploy.

148 of 148 new or added lines in 24 files covered. (100.0%)

3448 of 6821 relevant lines covered (50.55%)

0.51 hits per line

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

0.0
/src/main/java/com/smartsheet/api/internal/http/AndroidHttpClient.java
1
package com.smartsheet.api.internal.http;
2

3
/*
4
 * #[license]
5
 * Smartsheet Java SDK
6
 * %%
7
 * Copyright (C) 2023 Smartsheet
8
 * %%
9
 * Licensed under the Apache License, Version 2.0 (the "License");
10
 * you may not use this file except in compliance with the License.
11
 * You may obtain a copy of the License at
12
 *
13
 *      http://www.apache.org/licenses/LICENSE-2.0
14
 *
15
 * Unless required by applicable law or agreed to in writing, software
16
 * distributed under the License is distributed on an "AS IS" BASIS,
17
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
 * See the License for the specific language governing permissions and
19
 * limitations under the License.
20
 * %[license]
21
 */
22

23
import com.smartsheet.api.internal.json.JacksonJsonSerializer;
24
import com.smartsheet.api.internal.json.JsonSerializer;
25
import com.smartsheet.api.internal.util.StreamUtil;
26
import com.smartsheet.api.internal.util.Util;
27
import com.smartsheet.api.models.Error;
28
import okhttp3.MediaType;
29
import okhttp3.OkHttpClient;
30
import okhttp3.Request;
31
import okhttp3.RequestBody;
32
import okhttp3.Response;
33
import org.slf4j.Logger;
34
import org.slf4j.LoggerFactory;
35

36
import java.io.ByteArrayInputStream;
37
import java.io.ByteArrayOutputStream;
38
import java.io.IOException;
39
import java.io.InputStream;
40
import java.net.MalformedURLException;
41
import java.util.Map;
42
import java.util.Random;
43
import java.util.concurrent.TimeUnit;
44

45
public class AndroidHttpClient implements HttpClient {
46

47
    /** logger for general errors, warnings, etc */
48
    protected static final Logger logger = LoggerFactory.getLogger(AndroidHttpClient.class);
×
49

50
    private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");
×
51

52
    private static final String ERROR_OCCURRED = "Error occurred.";
53

54
    /**
55
     * Represents the underlying OkHttpClient.
56
     * <p>
57
     * It will be initialized in constructor and will not change afterwards.
58
     * </p>
59
     */
60
    private final OkHttpClient client;
61

62
    /** The okhttp http response. */
63
    private Response currentResponse;
64

65
    protected JsonSerializer jsonSerializer;
66

67
    protected long maxRetryTimeMillis = 15000;
×
68

69
    /**
70
     * Constructor.
71
     */
72
    public AndroidHttpClient() {
×
73
        this.client = new OkHttpClient.Builder()
×
74
                .connectTimeout(10, TimeUnit.SECONDS)
×
75
                .writeTimeout(10, TimeUnit.SECONDS)
×
76
                .readTimeout(30, TimeUnit.SECONDS)
×
77
                .build();
×
78
        this.jsonSerializer = new JacksonJsonSerializer();
×
79
    }
×
80

81
    /**
82
     * Log to the SLF4J logger (level based upon response status code). Override this function to add logging
83
     * or capture performance metrics.
84
     *
85
     * @param request request
86
     * @param response response
87
     * @param durationMillis response time in ms
88
     */
89
    public void logRequest(Request request, Response response, long durationMillis) {
90
        logger.info("{} {}, Response Code:{}, Request completed in {} ms", request.method(), request.url(),
×
91
                response.code(), durationMillis);
×
92
        if (response.code() != 200) {
×
93
            // log the request and response on error
94
            try {
95
                logger.warn(this.currentResponse.peekBody(4096).string());
×
96
            } catch (IOException e) {
×
97
                e.printStackTrace();
×
98
            }
×
99
        }
100
    }
×
101

102
    /**
103
     * Make an HTTP request and return the response.
104
     *
105
     * @param smartsheetRequest the smartsheet request
106
     * @return the HTTP response
107
     * @throws HttpClientException the HTTP client exception
108
     */
109
    @Override
110
    public HttpResponse request(HttpRequest smartsheetRequest) throws HttpClientException {
111
        Util.throwIfNull(smartsheetRequest);
×
112
        if (smartsheetRequest.getUri() == null) {
×
113
            throw new IllegalArgumentException("A Request URI is required.");
×
114
        }
115

116
        int attempt = 0;
×
117
        long start = System.currentTimeMillis();
×
118

119
        InputStream bodyStream = null;
×
120
        if (smartsheetRequest.getEntity() != null && smartsheetRequest.getEntity().getContent() != null) {
×
121
            bodyStream = smartsheetRequest.getEntity().getContent();
×
122
        }
123
        // the retry logic will consume the body stream so we make sure it supports mark/reset and mark it
124
        boolean canRetryRequest = bodyStream == null || bodyStream.markSupported();
×
125
        if (!canRetryRequest) {
×
126
            try {
127
                // attempt to wrap the body stream in a input-stream that does support mark/reset
128
                bodyStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(bodyStream));
×
129
                // close the old stream (just to be tidy) and then replace it with a reset-able stream
130
                smartsheetRequest.getEntity().getContent().close();
×
131
                smartsheetRequest.getEntity().setContent(bodyStream);
×
132
                canRetryRequest = true;
×
133
            } catch (IOException ignore) {
×
134
            }
×
135
        }
136

137
        HttpResponse smartsheetResponse;
138
        while (true) {
139

140
            // Create our new request
141
            Request.Builder builder = new Request.Builder();
×
142
            try {
143
                builder.url(smartsheetRequest.getUri().toURL());
×
144
            } catch (MalformedURLException e) {
×
145
                throw new HttpClientException(ERROR_OCCURRED, e);
×
146
            }
×
147

148
            // Clone our headers to request
149
            for (Map.Entry<String, String> entry : smartsheetRequest.getHeaders().entrySet()) {
×
150
                builder.addHeader(entry.getKey(), entry.getValue());
×
151
            }
×
152

153
            try {
154
                switch (smartsheetRequest.getMethod()) {
×
155
                    case GET:
156
                        builder.get();
×
157
                        break;
×
158
                    case POST:
159
                        builder.post(getRequestBody(smartsheetRequest));
×
160
                        break;
×
161
                    case PUT:
162
                        builder.put(getRequestBody(smartsheetRequest));
×
163
                        break;
×
164
                    case DELETE:
165
                        builder.delete();
×
166
                        break;
167
                }
168
            } catch (IOException e) {
×
169
                throw new HttpClientException(ERROR_OCCURRED, e);
×
170
            }
×
171

172
            // mark the body so we can reset on retry
173
            if (canRetryRequest && bodyStream != null) {
×
174
                bodyStream.mark((int) smartsheetRequest.getEntity().getContentLength());
×
175
            }
176

177
            try {
178
                // Create API request
179
                Request request = builder.build();
×
180
                long startTime = System.currentTimeMillis();
×
181
                this.currentResponse = client.newCall(request).execute();
×
182
                long endTime = System.currentTimeMillis();
×
183

184
                smartsheetResponse = new HttpResponse();
×
185
                smartsheetResponse.setStatusCode(this.currentResponse.code());
×
186
                if (this.currentResponse.body().contentLength() != 0) {
×
187
                    // Package response details
188
                    HttpEntity entity = new HttpEntity();
×
189
                    entity.setContentType(this.currentResponse.body().contentType().toString());
×
190
                    entity.setContentLength(this.currentResponse.body().contentLength());
×
191
                    entity.setContent(this.currentResponse.body().byteStream());
×
192
                    smartsheetResponse.setEntity(entity);
×
193
                }
194

195
                long responseTime = endTime - startTime;
×
196
                logRequest(request, this.currentResponse, responseTime);
×
197

198
                if (smartsheetResponse.getStatusCode() == 200) {
×
199
                    // call successful, exit the retry loop
200
                    break;
×
201
                }
202

203
                // the retry logic might consume the content stream so we make sure it supports mark/reset and mark it
204
                InputStream contentStream = smartsheetResponse.getEntity().getContent();
×
205
                if (!contentStream.markSupported()) {
×
206
                    // wrap the response stream in a input-stream that does support mark/reset
207
                    contentStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(contentStream));
×
208
                    // close the old stream (just to be tidy) and then replace it with a reset-able stream
209
                    smartsheetResponse.getEntity().getContent().close();
×
210
                    smartsheetResponse.getEntity().setContent(contentStream);
×
211
                }
212
                try {
213
                    contentStream.mark((int) smartsheetResponse.getEntity().getContentLength());
×
214
                    long timeSpent = System.currentTimeMillis() - start;
×
215
                    if (!shouldRetry(++attempt, timeSpent, smartsheetResponse)) {
×
216
                        // should not retry, or retry time exceeded, exit the retry loop
217
                        break;
218
                    }
219
                } finally {
220
                    if (bodyStream != null) {
×
221
                        bodyStream.reset();
×
222
                    }
223
                    contentStream.reset();
×
224
                }
225
                this.releaseConnection();
×
226

227
            } catch (IOException ex) {
×
228
                throw new HttpClientException(ERROR_OCCURRED, ex);
×
229
            }
×
230
        }
×
231
        return smartsheetResponse;
×
232
    }
233

234
    private RequestBody getRequestBody(HttpRequest apiRequest) throws IOException {
235
        int sizRead;
236
        byte[] buffer = new byte[16384];
×
237
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
×
238
        while ((sizRead = apiRequest.getEntity().getContent().read(buffer, 0, buffer.length)) != -1) {
×
239
            bao.write(buffer, 0, sizRead);
×
240
        }
241
        return RequestBody.create(MEDIA_TYPE_JSON, bao.toByteArray());
×
242
    }
243

244
    /**
245
     * Set the max retry time for API calls which fail and are retry-able.
246
     */
247
    public void setMaxRetryTimeMillis(long maxRetryTimeMillis) {
248
        this.maxRetryTimeMillis = maxRetryTimeMillis;
×
249
    }
×
250

251
    /**
252
     * The backoff calculation routine. Uses exponential backoff. If the maximum elapsed time
253
     * has expired, this calculation returns -1 causing the caller to fall out of the retry loop.
254
     * @return -1 to fall out of retry loop, positive number indicates backoff time
255
     */
256
    public long calcBackoff(int previousAttempts, long totalElapsedTimeMillis, Error error) {
257

258
        long backoffMillis = (long) (Math.pow(2, previousAttempts) * 1000) + new Random().nextInt(1000);
×
259

260
        if (totalElapsedTimeMillis + backoffMillis > maxRetryTimeMillis) {
×
261
            logger.info("Elapsed time " + totalElapsedTimeMillis + " + backoff time " + backoffMillis +
×
262
                    " exceeds max retry time " + maxRetryTimeMillis + ", exiting retry loop");
263
            return -1;
×
264
        }
265
        return backoffMillis;
×
266
    }
267

268
    /**
269
     * Called when an API request fails to determine if it can retry the request.
270
     * Calls calcBackoff to determine the time to wait in between retries.
271
     *
272
     * @param previousAttempts number of attempts (including this one) to execute request
273
     * @param totalElapsedTimeMillis total time spent in millis for all previous (and this) attempt
274
     * @param response the failed HttpResponse
275
     * @return true if this request can be retried
276
     */
277
    public boolean shouldRetry(int previousAttempts, long totalElapsedTimeMillis, HttpResponse response) {
278
        String contentType = response.getEntity().getContentType();
×
279
        if (contentType != null && !contentType.startsWith("application/json")) {
×
280
            // it's not JSON; don't even try to parse it
281
            return false;
×
282
        }
283
        Error error;
284
        try {
285
            error = jsonSerializer.deserialize(Error.class, response.getEntity().getContent());
×
286
        } catch (IOException e) {
×
287
            return false;
×
288
        }
×
289
        switch (error.getErrorCode()) {
×
290
            // Smartsheet.com is currently offline for system maintenance. Please check back again shortly.
291
            case 4001:
292
            // Server timeout exceeded. Request has failed
293
            case 4002:
294
            // Rate limit exceeded.
295
            case 4003:
296
            // An unexpected error has occurred. Please retry your request.
297
            // If you encounter this error repeatedly, please contact api@smartsheet.com for assistance.
298
            case 4004:
299
                break;
×
300
            default:
301
                return false;
×
302
        }
303

304
        long backoffMillis = calcBackoff(previousAttempts, totalElapsedTimeMillis, error);
×
305
        if (backoffMillis < 0) {
×
306
            return false;
×
307
        }
308

309
        logger.info("HttpError StatusCode=" + response.getStatusCode() + ": Retrying in " + backoffMillis + " milliseconds");
×
310
        try {
311
            Thread.sleep(backoffMillis);
×
312
        } catch (InterruptedException e) {
×
313
            logger.warn("sleep interrupted", e);
×
314
            return false;
×
315
        }
×
316
        return true;
×
317
    }
318

319
    /**
320
     * Close the HttpClient.
321
     */
322
    @Override
323
    public void close() {
324
        this.client.connectionPool().evictAll();
×
325
    }
×
326

327
    /* (non-Javadoc)
328
     * @see com.smartsheet.api.internal.http.HttpClient#releaseConnection()
329
     */
330
    @Override
331
    public void releaseConnection() {
332
        this.closeCurrentResponse();
×
333
    }
×
334

335
    private void closeCurrentResponse() {
336
        Response response = this.currentResponse;
×
337
        if (response != null) {
×
338
            if (response.body() != null) {
×
339
                response.body().close();
×
340
            }
341
            this.currentResponse = null;
×
342
        }
343
    }
×
344
}
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