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

smartsheet / smartsheet-java-sdk / #50

02 Oct 2023 04:35PM UTC coverage: 56.752% (+0.03%) from 56.722%
#50

push

github-actions

web-flow
Checkstyle fixes and refactor DefaultHttpClient#request (#72)

* Correct additional checkstyle issues and refactor DefaultHttpClient#request

* Reduce checkstyle errors to 5

* Update to add supressions instead of removing rules

45 of 45 new or added lines in 5 files covered. (100.0%)

3875 of 6828 relevant lines covered (56.75%)

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

17
package com.smartsheet.api.internal.http;
18

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

32
import java.io.ByteArrayInputStream;
33
import java.io.ByteArrayOutputStream;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import java.net.MalformedURLException;
37
import java.util.Map;
38
import java.util.Random;
39
import java.util.concurrent.TimeUnit;
40

41
public class AndroidHttpClient implements HttpClient {
42

43
    /** logger for general errors, warnings, etc */
44
    protected static final Logger logger = LoggerFactory.getLogger(AndroidHttpClient.class);
×
45

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

48
    private static final String ERROR_OCCURRED = "Error occurred.";
49

50
    /**
51
     * Represents the underlying OkHttpClient.
52
     * <p>
53
     * It will be initialized in constructor and will not change afterwards.
54
     * </p>
55
     */
56
    private final OkHttpClient client;
57

58
    /** The okhttp http response. */
59
    private Response currentResponse;
60

61
    protected JsonSerializer jsonSerializer;
62

63
    protected long maxRetryTimeMillis = 15000;
×
64

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

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

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

112
        int attempt = 0;
×
113
        long start = System.currentTimeMillis();
×
114

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

133
        HttpResponse smartsheetResponse;
134
        while (true) {
135

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

144
            // Clone our headers to request
145
            for (Map.Entry<String, String> entry : smartsheetRequest.getHeaders().entrySet()) {
×
146
                builder.addHeader(entry.getKey(), entry.getValue());
×
147
            }
×
148

149
            try {
150
                switch (smartsheetRequest.getMethod()) {
×
151
                    case GET:
152
                        builder.get();
×
153
                        break;
×
154
                    case POST:
155
                        builder.post(getRequestBody(smartsheetRequest));
×
156
                        break;
×
157
                    case PUT:
158
                        builder.put(getRequestBody(smartsheetRequest));
×
159
                        break;
×
160
                    case DELETE:
161
                        builder.delete();
×
162
                        break;
×
163
                    default:
164
                        // This switch is exhaustive, but the checkstyle doesn't know that
165
                        throw new UnsupportedOperationException("Unsupported method: " + smartsheetRequest.getMethod());
×
166
                }
167
            } catch (IOException e) {
×
168
                throw new HttpClientException(ERROR_OCCURRED, e);
×
169
            }
×
170

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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