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

smartsheet / smartsheet-java-sdk / #55

02 Oct 2024 07:40PM UTC coverage: 60.548% (+0.7%) from 59.836%
#55

push

github

web-flow
Release prep for 3.2.1 (#103)

4156 of 6864 relevant lines covered (60.55%)

0.61 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) 2024 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
    /**
44
     * logger for general errors, warnings, etc
45
     */
46
    protected static final Logger logger = LoggerFactory.getLogger(AndroidHttpClient.class);
×
47

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

50
    private static final String ERROR_OCCURRED = "Error occurred.";
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
    /**
61
     * The okhttp http response.
62
     */
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
                    default:
168
                        // This switch is exhaustive, but the checkstyle doesn't know that
169
                        throw new UnsupportedOperationException("Unsupported method: " + smartsheetRequest.getMethod());
×
170
                }
171
            } catch (IOException e) {
×
172
                throw new HttpClientException(ERROR_OCCURRED, e);
×
173
            }
×
174

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

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

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

198
                long responseTime = endTime - startTime;
×
199
                logRequest(request, this.currentResponse, responseTime);
×
200

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

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

230
            } catch (IOException ex) {
×
231
                throw new HttpClientException(ERROR_OCCURRED, ex);
×
232
            }
×
233
        }
×
234
        return smartsheetResponse;
×
235
    }
236

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

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

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

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

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

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

308
        long backoffMillis = calcBackoff(previousAttempts, totalElapsedTimeMillis, error);
×
309
        if (backoffMillis < 0) {
×
310
            return false;
×
311
        }
312

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

323
    /**
324
     * Close the HttpClient.
325
     */
326
    @Override
327
    public void close() {
328
        this.client.connectionPool().evictAll();
×
329
    }
×
330

331
    /* (non-Javadoc)
332
     * @see com.smartsheet.api.internal.http.HttpClient#releaseConnection()
333
     */
334
    @Override
335
    public void releaseConnection() {
336
        this.closeCurrentResponse();
×
337
    }
×
338

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