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

smartsheet / smartsheet-java-sdk / #53

08 Dec 2023 05:30PM UTC coverage: 57.712% (-0.3%) from 58.04%
#53

push

github

web-flow
Fix deploy Command (#77)

3940 of 6827 relevant lines covered (57.71%)

0.58 hits per line

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

66.84
/src/main/java/com/smartsheet/api/internal/http/DefaultHttpClient.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.Trace;
20
import com.smartsheet.api.internal.json.JacksonJsonSerializer;
21
import com.smartsheet.api.internal.json.JsonSerializer;
22
import com.smartsheet.api.internal.util.StreamUtil;
23
import com.smartsheet.api.internal.util.Util;
24
import com.smartsheet.api.models.Error;
25
import org.apache.http.Header;
26
import org.apache.http.NoHttpResponseException;
27
import org.apache.http.client.ClientProtocolException;
28
import org.apache.http.client.NonRepeatableRequestException;
29
import org.apache.http.client.config.RequestConfig;
30
import org.apache.http.client.methods.CloseableHttpResponse;
31
import org.apache.http.client.methods.HttpDelete;
32
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
33
import org.apache.http.client.methods.HttpGet;
34
import org.apache.http.client.methods.HttpPost;
35
import org.apache.http.client.methods.HttpPut;
36
import org.apache.http.client.methods.HttpRequestBase;
37
import org.apache.http.client.methods.HttpRequestWrapper;
38
import org.apache.http.entity.ContentType;
39
import org.apache.http.entity.InputStreamEntity;
40
import org.apache.http.impl.client.CloseableHttpClient;
41
import org.apache.http.impl.client.HttpClients;
42
import org.apache.http.protocol.BasicHttpContext;
43
import org.apache.http.protocol.HttpContext;
44
import org.jetbrains.annotations.Nullable;
45
import org.slf4j.Logger;
46
import org.slf4j.LoggerFactory;
47

48
import java.io.ByteArrayInputStream;
49
import java.io.IOException;
50
import java.io.InputStream;
51
import java.io.OutputStream;
52
import java.io.PrintWriter;
53
import java.util.HashMap;
54
import java.util.HashSet;
55
import java.util.Map;
56
import java.util.Random;
57
import java.util.Set;
58

59
/**
60
 * This is the Apache HttpClient based HttpClient implementation.
61
 * <p>
62
 * Thread Safety: This class is thread safe because it is immutable and the underlying Apache CloseableHttpClient is
63
 * thread safe.
64
 * @see <a href="http://hc.apache.org/httpcomponents-client-ga/index.html">Apache HttpClient</a>
65
 */
66
public class DefaultHttpClient implements HttpClient {
67

68
    /** logger for general errors, warnings, etc */
69
    protected static final Logger logger = LoggerFactory.getLogger(DefaultHttpClient.class);
1✔
70

71
    /** used by default retry/timeout logic and available for overriders */
72
    protected static final String JSON_MIME_TYPE = ContentType.APPLICATION_JSON.getMimeType();
1✔
73

74
    protected JsonSerializer jsonSerializer;
75

76
    protected long maxRetryTimeMillis = 15000;
1✔
77

78
    /**
79
     * Represents the underlying Apache CloseableHttpClient.
80
     * <p>
81
     * It will be initialized in constructor and will not change afterwards.
82
     */
83
    private final CloseableHttpClient httpClient;
84

85
    /** The apache http response. */
86
    private CloseableHttpResponse apacheHttpResponse;
87

88
    /** to avoid creating new sets for each call (we use Sets for practical and perf reasons) */
89
    private static final Set<Trace> REQUEST_RESPONSE_SUMMARY = Set.of(
1✔
90
            Trace.RequestHeaders,
91
            Trace.RequestBodySummary,
92
            Trace.ResponseHeaders,
93
            Trace.ResponseBodySummary
94
    );
95

96
    /** default values for trace-logging extracted from system-properties (can still be overwritten at the instance level) */
97
    private static final boolean TRACE_PRETTY_PRINT_DEFAULT = Boolean.parseBoolean(System.getProperty("Smartsheet.trace.pretty", "true"));
1✔
98

99
    // empty by default
100
    private static final Set<Trace> TRACE_DEFAULT_TRACE_SET = Trace.parse(System.getProperty("Smartsheet.trace.parts"));
1✔
101

102
    /** where to send trace logs */
103
    private static PrintWriter traceWriter;
104

105
    private final Random random = new Random();
1✔
106

107
    static {
108
        // default trace stream
109
        setTraceStream(System.out);
1✔
110
        if (!TRACE_DEFAULT_TRACE_SET.isEmpty()) {
1✔
111
            traceWriter.println("default trace logging - pretty:" + TRACE_PRETTY_PRINT_DEFAULT + " parts:" + TRACE_DEFAULT_TRACE_SET);
×
112
        }
113
    }
1✔
114

115
    /** the set of Trace levels to use in trace-logging */
116
    private final Set<Trace> traces = new HashSet<>(TRACE_DEFAULT_TRACE_SET);
1✔
117

118
    /** whether to log pretty or compact */
119
    private boolean tracePrettyPrint = TRACE_PRETTY_PRINT_DEFAULT;
1✔
120

121
    private static final String LOG_ARG = "{}";
122
    private static final String ERROR_OCCURRED = "Error occurred.";
123

124
    /**
125
     * Constructor.
126
     */
127
    public DefaultHttpClient() {
128
        this(HttpClients.createDefault(), new JacksonJsonSerializer());
1✔
129
    }
1✔
130

131
    /**
132
     * Constructor.
133
     * <p>
134
     * Parameters: - httpClient : the Apache CloseableHttpClient to use
135
     * <p>
136
     * Exceptions: - IllegalArgumentException : if any argument is null
137
     *
138
     * @param httpClient the http client
139
     */
140
    public DefaultHttpClient(CloseableHttpClient httpClient, JsonSerializer jsonSerializer) {
1✔
141
        this.httpClient = Util.throwIfNull(httpClient);
1✔
142
        this.jsonSerializer = jsonSerializer;
1✔
143
    }
1✔
144

145
    /**
146
     * Log to the SLF4J logger (level based upon response status code). Override this function to add logging
147
     * or capture performance metrics.
148
     *
149
     * @param request request
150
     * @param requestEntity request body
151
     * @param response response
152
     * @param responseEntity response body
153
     * @param durationMillis response time in ms
154
     */
155
    public void logRequest(HttpRequestBase request, HttpEntitySnapshot requestEntity,
156
                           HttpResponse response, HttpEntitySnapshot responseEntity, long durationMillis) throws IOException {
157

158
        logger.info("{} {}, Response Code:{}, Request completed in {} ms", request.getMethod(), request.getURI(),
1✔
159
                response.getStatusCode(), durationMillis);
1✔
160
        logger.debug(LOG_ARG, RequestAndResponseData.of(request, requestEntity, response, responseEntity, REQUEST_RESPONSE_SUMMARY));
1✔
161
    }
1✔
162

163
    /**
164
     * Make an HTTP request and return the response.
165
     *
166
     * @param smartsheetRequest the smartsheet request
167
     * @return the HTTP response
168
     * @throws HttpClientException the HTTP client exception
169
     */
170
    public HttpResponse request(HttpRequest smartsheetRequest) throws HttpClientException {
171
        Util.throwIfNull(smartsheetRequest);
1✔
172
        if (smartsheetRequest.getUri() == null) {
1✔
173
            throw new IllegalArgumentException("A Request URI is required.");
×
174
        }
175

176
        long start = System.currentTimeMillis();
1✔
177

178
        HttpRequestBase apacheHttpRequest;
179
        HttpResponse smartsheetResponse;
180

181
        InputStream bodyStream = null;
1✔
182
        if (smartsheetRequest.getEntity() != null && smartsheetRequest.getEntity().getContent() != null) {
1✔
183
            bodyStream = smartsheetRequest.getEntity().getContent();
1✔
184
        }
185
        // the retry logic will consume the body stream so we make sure it supports mark/reset and mark it
186
        boolean canRetryRequest = bodyStream == null || bodyStream.markSupported();
1✔
187
        if (!canRetryRequest) {
1✔
188
            try {
189
                // attempt to wrap the body stream in a input-stream that does support mark/reset
190
                bodyStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(bodyStream));
1✔
191
                // close the old stream (just to be tidy) and then replace it with a reset-able stream
192
                smartsheetRequest.getEntity().getContent().close();
1✔
193
                smartsheetRequest.getEntity().setContent(bodyStream);
1✔
194
                canRetryRequest = true;
1✔
195
            } catch (IOException ignore) {
1✔
196
            }
1✔
197
        }
198

199
        // the retry loop
200
        while (true) {
201
            int attempt = 0;
1✔
202
            apacheHttpRequest = createApacheRequest(smartsheetRequest);
1✔
203

204
            // Set HTTP headers
205
            if (smartsheetRequest.getHeaders() != null) {
1✔
206
                for (Map.Entry<String, String> header : smartsheetRequest.getHeaders().entrySet()) {
1✔
207
                    apacheHttpRequest.addHeader(header.getKey(), header.getValue());
1✔
208
                }
1✔
209
            }
210

211
            HttpEntitySnapshot responseEntityCopy = null;
1✔
212
            // Set HTTP entity
213
            HttpEntitySnapshot requestEntityCopy = copyRequestEntity(smartsheetRequest, apacheHttpRequest);
1✔
214

215
            // mark the body so we can reset on retry
216
            if (canRetryRequest && bodyStream != null) {
1✔
217
                bodyStream.mark((int) smartsheetRequest.getEntity().getContentLength());
1✔
218
            }
219

220
            // Make the HTTP request
221
            smartsheetResponse = new HttpResponse();
1✔
222
            HttpContext context = new BasicHttpContext();
1✔
223
            try {
224
                long startTime = System.currentTimeMillis();
1✔
225
                apacheHttpResponse = this.httpClient.execute(apacheHttpRequest, context);
1✔
226
                long endTime = System.currentTimeMillis();
1✔
227

228
                HttpEntitySnapshot newResponseCopy = updateWithResponse(apacheHttpRequest, context, smartsheetResponse);
1✔
229
                if (newResponseCopy != null) {
1✔
230
                    responseEntityCopy = newResponseCopy;
1✔
231
                }
232

233
                long responseTime = endTime - startTime;
1✔
234
                logRequest(apacheHttpRequest, requestEntityCopy, smartsheetResponse, responseEntityCopy, responseTime);
1✔
235

236
                // trace-logging of request and response (if so configured)
237
                if (traces.size() > 0) {
1✔
238
                    RequestAndResponseData requestAndResponseData = RequestAndResponseData.of(apacheHttpRequest,
×
239
                            requestEntityCopy, smartsheetResponse, responseEntityCopy, traces);
240
                    traceWriter.println(requestAndResponseData.toString(tracePrettyPrint));
×
241
                }
242

243
                if (smartsheetResponse.getStatusCode() == 200) {
1✔
244
                    // call successful, exit the retry loop
245
                    break;
1✔
246
                }
247

248
                // the retry logic might consume the content stream so we make sure it supports mark/reset and mark it
249
                InputStream contentStream = smartsheetResponse.getEntity().getContent();
1✔
250
                if (!contentStream.markSupported()) {
1✔
251
                    // wrap the response stream in a input-stream that does support mark/reset
252
                    contentStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(contentStream));
×
253
                    // close the old stream (just to be tidy) and then replace it with a reset-able stream
254
                    smartsheetResponse.getEntity().getContent().close();
×
255
                    smartsheetResponse.getEntity().setContent(contentStream);
×
256
                }
257
                try {
258
                    contentStream.mark((int) smartsheetResponse.getEntity().getContentLength());
1✔
259
                    long timeSpent = System.currentTimeMillis() - start;
1✔
260
                    if (!shouldRetry(++attempt, timeSpent, smartsheetResponse)) {
1✔
261
                        // should not retry, or retry time exceeded, exit the retry loop
262
                        break;
263
                    }
264
                } finally {
265
                    if (bodyStream != null) {
1✔
266
                        bodyStream.reset();
1✔
267
                    }
268
                    contentStream.reset();
1✔
269
                }
270
                // moving this to finally causes issues because socket is closed (which means response stream is closed)
271
                this.releaseConnection();
×
272

273
            } catch (ClientProtocolException e) {
1✔
274
                logger.warn("ClientProtocolException " + e.getMessage());
1✔
275
                logger.warn(LOG_ARG, RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
1✔
276
                        responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
277
                try {
278
                    // if this is a PUT and was retried by the http client, the body content stream is at the
279
                    // end and is a NonRepeatableRequest. If we marked the body content stream prior to execute,
280
                    // reset and retry
281
                    if (canRetryRequest && e.getCause() instanceof NonRepeatableRequestException) {
1✔
282
                        if (smartsheetRequest.getEntity() != null) {
×
283
                            smartsheetRequest.getEntity().getContent().reset();
×
284
                        }
285
                        continue;
×
286
                    }
287
                } catch (IOException ignore) {
×
288
                }
1✔
289
                throw new HttpClientException(ERROR_OCCURRED, e);
1✔
290
            } catch (NoHttpResponseException e) {
×
291
                logger.warn("NoHttpResponseException {}", e.getMessage());
×
292
                logger.warn(LOG_ARG, RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
×
293
                        responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
294
                try {
295
                    // check to see if the response was empty and this was a POST. All other HTTP methods
296
                    // will be automatically retried by the http client.
297
                    // (POST is non-idempotent and is not retried automatically, but is safe for us to retry)
298
                    if (canRetryRequest && smartsheetRequest.getMethod() == HttpMethod.POST) {
×
299
                        if (smartsheetRequest.getEntity() != null) {
×
300
                            smartsheetRequest.getEntity().getContent().reset();
×
301
                        }
302
                        continue;
×
303
                    }
304
                } catch (IOException ignore) {
×
305
                }
×
306
                throw new HttpClientException(ERROR_OCCURRED, e);
×
307
            } catch (IOException e) {
×
308
                logger.warn(LOG_ARG, RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
×
309
                        responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
310
                throw new HttpClientException(ERROR_OCCURRED, e);
×
311
            }
×
312
        }
×
313
        return smartsheetResponse;
1✔
314
    }
315

316
    private HttpEntitySnapshot copyRequestEntity(HttpRequest smartsheetRequest, HttpRequestBase apacheHttpRequest) {
317
        final HttpEntity entity = smartsheetRequest.getEntity();
1✔
318
        HttpEntitySnapshot requestEntityCopy = null;
1✔
319
        if (apacheHttpRequest instanceof HttpEntityEnclosingRequestBase && entity != null && entity.getContent() != null) {
1✔
320
            try {
321
                // we need access to the original request stream so we can log it (in the event of errors and/or tracing)
322
                requestEntityCopy = new HttpEntitySnapshot(entity);
1✔
323
            } catch (IOException iox) {
×
324
                logger.error("failed to make copy of original request entity", iox);
×
325
            }
1✔
326

327
            InputStreamEntity streamEntity = new InputStreamEntity(entity.getContent(), entity.getContentLength());
1✔
328
            // why?  not supported by library?
329
            streamEntity.setChunked(false);
1✔
330
            ((HttpEntityEnclosingRequestBase) apacheHttpRequest).setEntity(streamEntity);
1✔
331
        }
332
        return requestEntityCopy;
1✔
333
    }
334

335
    @Nullable
336
    private HttpEntitySnapshot updateWithResponse(HttpRequestBase apacheHttpRequest, HttpContext context,
337
                                                  HttpResponse smartsheetResponse) throws IOException {
338
        // Set request headers to values ACTUALLY SENT (not just created by us), this would include:
339
        // 'Connection', 'Accept-Encoding', etc. However, if a proxy is used, this may be the proxy's CONNECT
340
        // request, hence the test for HTTP method first
341
        Object httpRequest = context.getAttribute("http.request");
1✔
342
        if (httpRequest != null && HttpRequestWrapper.class.isAssignableFrom(httpRequest.getClass())) {
1✔
343
            HttpRequestWrapper actualRequest = (HttpRequestWrapper) httpRequest;
1✔
344
            switch (HttpMethod.valueOf(actualRequest.getMethod())) {
1✔
345
                case GET:
346
                case POST:
347
                case PUT:
348
                case DELETE:
349
                    apacheHttpRequest.setHeaders(actualRequest.getAllHeaders());
1✔
350
                    break;
1✔
351
                default:
352
                    break;
353
            }
354
        }
355

356
        // Set returned headers
357
        smartsheetResponse.setHeaders(new HashMap<>());
1✔
358
        for (Header header : apacheHttpResponse.getAllHeaders()) {
1✔
359
            smartsheetResponse.getHeaders().put(header.getName(), header.getValue());
1✔
360
        }
361
        smartsheetResponse.setStatus(apacheHttpResponse.getStatusLine().getStatusCode(),
1✔
362
                apacheHttpResponse.getStatusLine().toString());
1✔
363

364
        // Set returned entities
365
        if (apacheHttpResponse.getEntity() != null) {
1✔
366
            HttpEntity httpEntity = new HttpEntity();
1✔
367
            httpEntity.setContentType(apacheHttpResponse.getEntity().getContentType().getValue());
1✔
368
            httpEntity.setContentLength(apacheHttpResponse.getEntity().getContentLength());
1✔
369
            httpEntity.setContent(apacheHttpResponse.getEntity().getContent());
1✔
370
            smartsheetResponse.setEntity(httpEntity);
1✔
371
            return new HttpEntitySnapshot(httpEntity);
1✔
372
        }
373

374
        return null;
×
375
    }
376

377
    /**
378
     * Create the Apache HTTP request. Override this function to inject additional
379
     * haaders in the request or use a proxy.
380
     *
381
     * @param smartsheetRequest (request method and base URI come from here)
382
     * @return the Apache HTTP request
383
     */
384
    public HttpRequestBase createApacheRequest(HttpRequest smartsheetRequest) {
385
        HttpRequestBase apacheHttpRequest;
386

387
        // Create Apache HTTP request based on the smartsheetRequest request type
388
        switch (smartsheetRequest.getMethod()) {
1✔
389
            case GET:
390
                apacheHttpRequest = new HttpGet(smartsheetRequest.getUri());
1✔
391
                break;
1✔
392
            case POST:
393
                apacheHttpRequest = new HttpPost(smartsheetRequest.getUri());
1✔
394
                break;
1✔
395
            case PUT:
396
                apacheHttpRequest = new HttpPut(smartsheetRequest.getUri());
1✔
397
                break;
1✔
398
            case DELETE:
399
                apacheHttpRequest = new HttpDelete(smartsheetRequest.getUri());
1✔
400
                break;
1✔
401
            default:
402
                throw new UnsupportedOperationException("Request method " + smartsheetRequest.getMethod() + " is not supported!");
×
403
        }
404

405
        RequestConfig.Builder builder = RequestConfig.custom();
1✔
406
        if (apacheHttpRequest.getConfig() != null) {
1✔
407
            builder = RequestConfig.copy(apacheHttpRequest.getConfig());
×
408
        }
409
        builder.setRedirectsEnabled(true);
1✔
410
        RequestConfig config = builder.build();
1✔
411
        apacheHttpRequest.setConfig(config);
1✔
412
        return apacheHttpRequest;
1✔
413
    }
414

415
    /**
416
     * Set the max retry time for API calls which fail and are retry-able.
417
     */
418
    public void setMaxRetryTimeMillis(long maxRetryTimeMillis) {
419
        this.maxRetryTimeMillis = maxRetryTimeMillis;
×
420
    }
×
421

422
    /**
423
     * The backoff calculation routine. Uses exponential backoff. If the maximum elapsed time
424
     * has expired, this calculation returns -1 causing the caller to fall out of the retry loop.
425
     * @return -1 to fall out of retry loop, positive number indicates backoff time
426
     */
427
    public long calcBackoff(int previousAttempts, long totalElapsedTimeMillis, Error error) {
428

429
        long backoffMillis = (long) (Math.pow(2, previousAttempts) * 1000) + random.nextInt(1000);
×
430

431
        if (totalElapsedTimeMillis + backoffMillis > maxRetryTimeMillis) {
×
432
            logger.info(
×
433
                    "Elapsed time {} + backoff time {} exceeds max retry time {}, exiting retry loop",
434
                    totalElapsedTimeMillis,
×
435
                    backoffMillis,
×
436
                    maxRetryTimeMillis
×
437
            );
438
            return -1;
×
439
        }
440
        return backoffMillis;
×
441
    }
442

443
    /**
444
     * Called when an API request fails to determine if it can retry the request.
445
     * Calls calcBackoff to determine the time to wait in between retries.
446
     *
447
     * @param previousAttempts number of attempts (including this one) to execute request
448
     * @param totalElapsedTimeMillis total time spent in millis for all previous (and this) attempt
449
     * @param response the failed HttpResponse
450
     * @return true if this request can be retried
451
     */
452
    public boolean shouldRetry(int previousAttempts, long totalElapsedTimeMillis, HttpResponse response) {
453
        String contentType = response.getEntity().getContentType();
1✔
454
        if (contentType != null && !contentType.startsWith(JSON_MIME_TYPE)) {
1✔
455
            // it's not JSON; don't even try to parse it
456
            return false;
×
457
        }
458
        Error error;
459
        try {
460
            error = jsonSerializer.deserialize(Error.class, response.getEntity().getContent());
1✔
461
        } catch (IOException e) {
×
462
            return false;
×
463
        }
1✔
464
        switch (error.getErrorCode()) {
1✔
465
            case 4001:
466
                // Smartsheet.com is currently offline for system maintenance. Please check back again shortly.
467
            case 4002:
468
                // Server timeout exceeded. Request has failed
469
            case 4003:
470
                // Rate limit exceeded.
471
            case 4004:
472
                // An unexpected error has occurred. Please retry your request
473
                // If you encounter this error repeatedly, please contact api@smartsheet.com for assistance
474
                break;
×
475
            default:
476
                return false;
1✔
477
        }
478

479
        long backoffMillis = calcBackoff(previousAttempts, totalElapsedTimeMillis, error);
×
480
        if (backoffMillis < 0) {
×
481
            return false;
×
482
        }
483

484
        logger.info("HttpError StatusCode={}: Retrying in {} milliseconds", response.getStatusCode(), backoffMillis);
×
485
        try {
486
            Thread.sleep(backoffMillis);
×
487
        } catch (InterruptedException e) {
×
488
            logger.warn("sleep interrupted", e);
×
489
            return false;
×
490
        }
×
491
        return true;
×
492
    }
493

494
    /**
495
     * Close the HttpClient.
496
     *
497
     * @throws IOException Signals that an I/O exception has occurred.
498
     */
499
    @Override
500
    public void close() throws IOException {
501
        this.httpClient.close();
1✔
502
    }
1✔
503

504
    /* (non-Javadoc)
505
     * @see com.smartsheet.api.internal.http.HttpClient#releaseConnection()
506
     */
507
    @Override
508
    public void releaseConnection() {
509
        if (apacheHttpResponse != null) {
1✔
510
            try {
511
                apacheHttpResponse.close();
1✔
512
                apacheHttpResponse = null;
1✔
513
            } catch (IOException e) {
×
514
                logger.error("error closing Apache HttpResponse", e);
×
515
            }
1✔
516
        }
517
    }
1✔
518

519
    /**
520
     * set the traces for this client
521
     * @param traces the fields to include in trace-logging
522
     */
523
    public void setTraces(Trace... traces) {
524
        this.traces.clear();
×
525
        for (Trace trace : traces) {
×
526
            if (!trace.addReplacements(this.traces)) {
×
527
                this.traces.add(trace);
×
528
            }
529
        }
530
    }
×
531

532
    /**
533
     * set whether to use nicely-formatted JSON or more compact format JSON in trace logging
534
     * @param pretty whether to print JSON in a "pretty" format or compact
535
     */
536
    public void setTracePrettyPrint(boolean pretty) {
537
        tracePrettyPrint = pretty;
×
538
    }
×
539

540
    /** only included for testing purposes */
541
    public static void setTraceStream(OutputStream traceStream) {
542
        traceWriter = new PrintWriter(traceStream, true);
1✔
543
    }
1✔
544
}
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