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

smartsheet / smartsheet-java-sdk / #41

24 Aug 2023 04:59PM UTC coverage: 50.458% (+0.01%) from 50.444%
#41

push

github-actions

web-flow
Fix Checkstyle Violations in "Impl" Classes (#53)

241 of 241 new or added lines in 32 files covered. (100.0%)

3417 of 6772 relevant lines covered (50.46%)

0.5 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
    /**
53
     * Represents the underlying OkHttpClient.
54
     * <p>
55
     * It will be initialized in constructor and will not change afterwards.
56
     * </p>
57
     */
58
    private final OkHttpClient client;
59

60
    /** The okhttp http response. */
61
    private Response currentResponse;
62

63
    protected JsonSerializer jsonSerializer;
64

65
    protected long maxRetryTimeMillis = 15000;
×
66

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

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

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

114
        int attempt = 0;
×
115
        long start = System.currentTimeMillis();
×
116

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

135
        HttpResponse smartsheetResponse;
136
        while (true) {
137

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

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

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

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

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

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

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

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

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

225
            } catch (IOException ex) {
×
226
                throw new HttpClientException("Error occurred.", ex);
×
227
            }
×
228
        }
×
229
        return smartsheetResponse;
×
230
    }
231

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

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

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

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc