• 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

67.19
/src/main/java/com/smartsheet/api/internal/http/DefaultHttpClient.java
1
package com.smartsheet.api.internal.http;
2

3
/*
4
 * #[license]
5
 * Smartsheet SDK for Java
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.Trace;
24
import com.smartsheet.api.internal.json.JacksonJsonSerializer;
25
import com.smartsheet.api.internal.json.JsonSerializer;
26
import com.smartsheet.api.internal.util.StreamUtil;
27
import com.smartsheet.api.internal.util.Util;
28
import com.smartsheet.api.models.Error;
29
import org.apache.http.Header;
30
import org.apache.http.NoHttpResponseException;
31
import org.apache.http.client.ClientProtocolException;
32
import org.apache.http.client.NonRepeatableRequestException;
33
import org.apache.http.client.config.RequestConfig;
34
import org.apache.http.client.methods.CloseableHttpResponse;
35
import org.apache.http.client.methods.HttpDelete;
36
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
37
import org.apache.http.client.methods.HttpGet;
38
import org.apache.http.client.methods.HttpPost;
39
import org.apache.http.client.methods.HttpPut;
40
import org.apache.http.client.methods.HttpRequestBase;
41
import org.apache.http.client.methods.HttpRequestWrapper;
42
import org.apache.http.entity.ContentType;
43
import org.apache.http.entity.InputStreamEntity;
44
import org.apache.http.impl.client.CloseableHttpClient;
45
import org.apache.http.impl.client.HttpClients;
46
import org.apache.http.protocol.BasicHttpContext;
47
import org.apache.http.protocol.HttpContext;
48
import org.slf4j.Logger;
49
import org.slf4j.LoggerFactory;
50

51
import java.io.ByteArrayInputStream;
52
import java.io.IOException;
53
import java.io.InputStream;
54
import java.io.OutputStream;
55
import java.io.PrintWriter;
56
import java.util.Arrays;
57
import java.util.Collections;
58
import java.util.HashMap;
59
import java.util.HashSet;
60
import java.util.Map;
61
import java.util.Random;
62
import java.util.Set;
63

64
/**
65
 * This is the Apache HttpClient (http://hc.apache.org/httpcomponents-client-ga/index.html) based HttpClient
66
 * implementation.
67
 *
68
 * Thread Safety: This class is thread safe because it is immutable and the underlying Apache CloseableHttpClient is
69
 * thread safe.
70
 */
71
public class DefaultHttpClient implements HttpClient {
72

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

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

79
    protected JsonSerializer jsonSerializer;
80

81
    protected long maxRetryTimeMillis = 15000;
1✔
82

83
    /**
84
     * Represents the underlying Apache CloseableHttpClient.
85
     * <p>
86
     * It will be initialized in constructor and will not change afterwards.
87
     */
88
    private final CloseableHttpClient httpClient;
89

90
    /** The apache http response. */
91
    private CloseableHttpResponse apacheHttpResponse;
92

93
    /** to avoid creating new sets for each call (we use Sets for practical and perf reasons) */
94
    private static final Set<Trace> REQUEST_RESPONSE_SUMMARY = Collections.unmodifiableSet(new HashSet<>(
1✔
95
            Arrays.asList(Trace.RequestHeaders, Trace.RequestBodySummary, Trace.ResponseHeaders, Trace.ResponseBodySummary)));
1✔
96

97
    private static final Set<Trace> REQUEST_RESPONSE = Collections.unmodifiableSet(new HashSet<>(
1✔
98
            Arrays.asList(Trace.RequestHeaders, Trace.RequestBody, Trace.ResponseHeaders, Trace.ResponseBody)));
1✔
99

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

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

106
    /** where to send trace logs */
107
    private static PrintWriter TRACE_WRITER;
108

109
    static {
110
        // default trace stream
111
        setTraceStream(System.out);
1✔
112
        if (TRACE_DEFAULT_TRACE_SET.size() > 0) {
1✔
113
            TRACE_WRITER.println("default trace logging - pretty:" + TRACE_PRETTY_PRINT_DEFAULT + " parts:" + TRACE_DEFAULT_TRACE_SET);
×
114
        }
115
    }
1✔
116

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

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

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

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

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

157
        logger.info("{} {}, Response Code:{}, Request completed in {} ms", request.getMethod(), request.getURI(),
1✔
158
                response.getStatusCode(), durationMillis);
1✔
159
        if (response.getStatusCode() != 200) {
1✔
160
            // log the request and response on error
161
            logger.warn("{}", RequestAndResponseData.of(request, requestEntity, response, responseEntity, REQUEST_RESPONSE));
1✔
162
        } else {
163
            // log the summary request and response on success
164
            logger.debug("{}", RequestAndResponseData.of(request, requestEntity, response, responseEntity, REQUEST_RESPONSE_SUMMARY));
1✔
165
        }
166
    }
1✔
167

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

181
        int attempt = 0;
1✔
182
        long start = System.currentTimeMillis();
1✔
183

184
        HttpRequestBase apacheHttpRequest;
185
        HttpResponse smartsheetResponse;
186

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

205
        // the retry loop
206
        while (true) {
207

208
            apacheHttpRequest = createApacheRequest(smartsheetRequest);
1✔
209

210
            // Set HTTP headers
211
            if (smartsheetRequest.getHeaders() != null) {
1✔
212
                for (Map.Entry<String, String> header : smartsheetRequest.getHeaders().entrySet()) {
1✔
213
                    apacheHttpRequest.addHeader(header.getKey(), header.getValue());
1✔
214
                }
1✔
215
            }
216

217
            HttpEntitySnapshot requestEntityCopy = null;
1✔
218
            HttpEntitySnapshot responseEntityCopy = null;
1✔
219
            // Set HTTP entity
220
            final HttpEntity entity = smartsheetRequest.getEntity();
1✔
221
            if (apacheHttpRequest instanceof HttpEntityEnclosingRequestBase && entity != null && entity.getContent() != null) {
1✔
222
                try {
223
                    // we need access to the original request stream so we can log it (in the event of errors and/or tracing)
224
                    requestEntityCopy = new HttpEntitySnapshot(entity);
1✔
225
                } catch (IOException iox) {
×
226
                    logger.error("failed to make copy of original request entity", iox);
×
227
                }
1✔
228

229
                InputStreamEntity streamEntity = new InputStreamEntity(entity.getContent(), entity.getContentLength());
1✔
230
                // why?  not supported by library?
231
                streamEntity.setChunked(false);
1✔
232
                ((HttpEntityEnclosingRequestBase) apacheHttpRequest).setEntity(streamEntity);
1✔
233
            }
234

235
            // mark the body so we can reset on retry
236
            if (canRetryRequest && bodyStream != null) {
1✔
237
                bodyStream.mark((int) smartsheetRequest.getEntity().getContentLength());
1✔
238
            }
239

240
            // Make the HTTP request
241
            smartsheetResponse = new HttpResponse();
1✔
242
            HttpContext context = new BasicHttpContext();
1✔
243
            try {
244
                long startTime = System.currentTimeMillis();
1✔
245
                apacheHttpResponse = this.httpClient.execute(apacheHttpRequest, context);
1✔
246
                long endTime = System.currentTimeMillis();
1✔
247

248
                // Set request headers to values ACTUALLY SENT (not just created by us), this would include:
249
                // 'Connection', 'Accept-Encoding', etc. However, if a proxy is used, this may be the proxy's CONNECT
250
                // request, hence the test for HTTP method first
251
                Object httpRequest = context.getAttribute("http.request");
1✔
252
                if (httpRequest != null && HttpRequestWrapper.class.isAssignableFrom(httpRequest.getClass())) {
1✔
253
                    HttpRequestWrapper actualRequest = (HttpRequestWrapper) httpRequest;
1✔
254
                    switch (HttpMethod.valueOf(actualRequest.getMethod())) {
1✔
255
                        case GET:
256
                        case POST:
257
                        case PUT:
258
                        case DELETE:
259
                            apacheHttpRequest.setHeaders(((HttpRequestWrapper) httpRequest).getAllHeaders());
1✔
260
                            break;
261
                    }
262
                }
263

264
                // Set returned headers
265
                smartsheetResponse.setHeaders(new HashMap<>());
1✔
266
                for (Header header : apacheHttpResponse.getAllHeaders()) {
1✔
267
                    smartsheetResponse.getHeaders().put(header.getName(), header.getValue());
1✔
268
                }
269
                smartsheetResponse.setStatus(apacheHttpResponse.getStatusLine().getStatusCode(),
1✔
270
                        apacheHttpResponse.getStatusLine().toString());
1✔
271

272
                // Set returned entities
273
                if (apacheHttpResponse.getEntity() != null) {
1✔
274
                    HttpEntity httpEntity = new HttpEntity();
1✔
275
                    httpEntity.setContentType(apacheHttpResponse.getEntity().getContentType().getValue());
1✔
276
                    httpEntity.setContentLength(apacheHttpResponse.getEntity().getContentLength());
1✔
277
                    httpEntity.setContent(apacheHttpResponse.getEntity().getContent());
1✔
278
                    smartsheetResponse.setEntity(httpEntity);
1✔
279
                    responseEntityCopy = new HttpEntitySnapshot(httpEntity);
1✔
280
                }
281

282
                long responseTime = endTime - startTime;
1✔
283
                logRequest(apacheHttpRequest, requestEntityCopy, smartsheetResponse, responseEntityCopy, responseTime);
1✔
284

285
                // trace-logging of request and response (if so configured)
286
                if (traces.size() > 0) {
1✔
287
                    RequestAndResponseData requestAndResponseData = RequestAndResponseData.of(apacheHttpRequest,
×
288
                            requestEntityCopy, smartsheetResponse, responseEntityCopy, traces);
289
                    TRACE_WRITER.println(requestAndResponseData.toString(tracePrettyPrint));
×
290
                }
291

292
                if (smartsheetResponse.getStatusCode() == 200) {
1✔
293
                    // call successful, exit the retry loop
294
                    break;
1✔
295
                }
296

297
                // the retry logic might consume the content stream so we make sure it supports mark/reset and mark it
298
                InputStream contentStream = smartsheetResponse.getEntity().getContent();
1✔
299
                if (!contentStream.markSupported()) {
1✔
300
                    // wrap the response stream in a input-stream that does support mark/reset
301
                    contentStream = new ByteArrayInputStream(StreamUtil.readBytesFromStream(contentStream));
×
302
                    // close the old stream (just to be tidy) and then replace it with a reset-able stream
303
                    smartsheetResponse.getEntity().getContent().close();
×
304
                    smartsheetResponse.getEntity().setContent(contentStream);
×
305
                }
306
                try {
307
                    contentStream.mark((int) smartsheetResponse.getEntity().getContentLength());
1✔
308
                    long timeSpent = System.currentTimeMillis() - start;
1✔
309
                    if (!shouldRetry(++attempt, timeSpent, smartsheetResponse)) {
1✔
310
                        // should not retry, or retry time exceeded, exit the retry loop
311
                        break;
312
                    }
313
                } finally {
314
                    if (bodyStream != null) {
1✔
315
                        bodyStream.reset();
1✔
316
                    }
317
                    contentStream.reset();
1✔
318
                }
319
                // moving this to finally causes issues because socket is closed (which means response stream is closed)
320
                this.releaseConnection();
×
321

322
            } catch (ClientProtocolException e) {
1✔
323
                try {
324
                    logger.warn("ClientProtocolException " + e.getMessage());
1✔
325
                    logger.warn("{}", RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
1✔
326
                            responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
327
                    // if this is a PUT and was retried by the http client, the body content stream is at the
328
                    // end and is a NonRepeatableRequest. If we marked the body content stream prior to execute,
329
                    // reset and retry
330
                    if (canRetryRequest && e.getCause() instanceof NonRepeatableRequestException) {
1✔
331
                        if (smartsheetRequest.getEntity() != null) {
×
332
                            smartsheetRequest.getEntity().getContent().reset();
×
333
                        }
334
                        continue;
×
335
                    }
336
                } catch (IOException ignore) {
×
337
                }
1✔
338
                throw new HttpClientException("Error occurred.", e);
1✔
339
            } catch (NoHttpResponseException e) {
×
340
                try {
341
                    logger.warn("NoHttpResponseException " + e.getMessage());
×
342
                    logger.warn("{}", RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
×
343
                            responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
344
                    // check to see if the response was empty and this was a POST. All other HTTP methods
345
                    // will be automatically retried by the http client.
346
                    // (POST is non-idempotent and is not retried automatically, but is safe for us to retry)
347
                    if (canRetryRequest && smartsheetRequest.getMethod() == HttpMethod.POST) {
×
348
                        if (smartsheetRequest.getEntity() != null) {
×
349
                            smartsheetRequest.getEntity().getContent().reset();
×
350
                        }
351
                        continue;
×
352
                    }
353
                } catch (IOException ignore) {
×
354
                }
×
355
                throw new HttpClientException("Error occurred.", e);
×
356
            } catch (IOException e) {
×
357
                try {
358
                    logger.warn("{}", RequestAndResponseData.of(apacheHttpRequest, requestEntityCopy, smartsheetResponse,
×
359
                            responseEntityCopy, REQUEST_RESPONSE_SUMMARY));
360
                } catch (IOException ignore) {
×
361
                }
×
362
                throw new HttpClientException("Error occurred.", e);
×
363
            }
×
364
        }
×
365
        return smartsheetResponse;
1✔
366
    }
367

368
    /**
369
     * Create the Apache HTTP request. Override this function to inject additional
370
     * haaders in the request or use a proxy.
371
     *
372
     * @param smartsheetRequest (request method and base URI come from here)
373
     * @return the Apache HTTP request
374
     */
375
    public HttpRequestBase createApacheRequest(HttpRequest smartsheetRequest) {
376
        HttpRequestBase apacheHttpRequest;
377

378
        // Create Apache HTTP request based on the smartsheetRequest request type
379
        switch (smartsheetRequest.getMethod()) {
1✔
380
            case GET:
381
                apacheHttpRequest = new HttpGet(smartsheetRequest.getUri());
1✔
382
                break;
1✔
383
            case POST:
384
                apacheHttpRequest = new HttpPost(smartsheetRequest.getUri());
1✔
385
                break;
1✔
386
            case PUT:
387
                apacheHttpRequest = new HttpPut(smartsheetRequest.getUri());
1✔
388
                break;
1✔
389
            case DELETE:
390
                apacheHttpRequest = new HttpDelete(smartsheetRequest.getUri());
1✔
391
                break;
1✔
392
            default:
393
                throw new UnsupportedOperationException("Request method " + smartsheetRequest.getMethod() + " is not supported!");
×
394
        }
395

396
        RequestConfig.Builder builder = RequestConfig.custom();
1✔
397
        if (apacheHttpRequest.getConfig() != null) {
1✔
398
            builder = RequestConfig.copy(apacheHttpRequest.getConfig());
×
399
        }
400
        builder.setRedirectsEnabled(true);
1✔
401
        RequestConfig config = builder.build();
1✔
402
        apacheHttpRequest.setConfig(config);
1✔
403
        return apacheHttpRequest;
1✔
404
    }
405

406
    /**
407
     * Set the max retry time for API calls which fail and are retry-able.
408
     */
409
    public void setMaxRetryTimeMillis(long maxRetryTimeMillis) {
410
        this.maxRetryTimeMillis = maxRetryTimeMillis;
×
411
    }
×
412

413
    /**
414
     * The backoff calculation routine. Uses exponential backoff. If the maximum elapsed time
415
     * has expired, this calculation returns -1 causing the caller to fall out of the retry loop.
416
     * @return -1 to fall out of retry loop, positive number indicates backoff time
417
     */
418
    public long calcBackoff(int previousAttempts, long totalElapsedTimeMillis, Error error) {
419

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

422
        if (totalElapsedTimeMillis + backoffMillis > maxRetryTimeMillis) {
×
423
            logger.info("Elapsed time " + totalElapsedTimeMillis + " + backoff time " + backoffMillis +
×
424
                    " exceeds max retry time " + maxRetryTimeMillis + ", exiting retry loop");
425
            return -1;
×
426
        }
427
        return backoffMillis;
×
428
    }
429

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

466
        long backoffMillis = calcBackoff(previousAttempts, totalElapsedTimeMillis, error);
×
467
        if (backoffMillis < 0) {
×
468
            return false;
×
469
        }
470

471
        logger.info("HttpError StatusCode=" + response.getStatusCode() + ": Retrying in " + backoffMillis + " milliseconds");
×
472
        try {
473
            Thread.sleep(backoffMillis);
×
474
        } catch (InterruptedException e) {
×
475
            logger.warn("sleep interrupted", e);
×
476
            return false;
×
477
        }
×
478
        return true;
×
479
    }
480

481
    /**
482
     * Close the HttpClient.
483
     *
484
     * @throws IOException Signals that an I/O exception has occurred.
485
     */
486
    @Override
487
    public void close() throws IOException {
488
        this.httpClient.close();
1✔
489
    }
1✔
490

491
    /* (non-Javadoc)
492
     * @see com.smartsheet.api.internal.http.HttpClient#releaseConnection()
493
     */
494
    @Override
495
    public void releaseConnection() {
496
        if (apacheHttpResponse != null) {
1✔
497
            try {
498
                apacheHttpResponse.close();
1✔
499
                apacheHttpResponse = null;
1✔
500
            } catch (IOException e) {
×
501
                logger.error("error closing Apache HttpResponse", e);
×
502
            }
1✔
503
        }
504
    }
1✔
505

506
    /**
507
     * set the traces for this client
508
     * @param traces the fields to include in trace-logging
509
     */
510
    public void setTraces(Trace... traces) {
511
        this.traces.clear();
×
512
        for (Trace trace : traces) {
×
513
            if (!trace.addReplacements(this.traces)) {
×
514
                this.traces.add(trace);
×
515
            }
516
        }
517
    }
×
518

519
    /**
520
     * set whether to use nicely-formatted JSON or more compact format JSON in trace logging
521
     * @param pretty whether to print JSON in a "pretty" format or compact
522
     */
523
    public void setTracePrettyPrint(boolean pretty) {
524
        tracePrettyPrint = pretty;
×
525
    }
×
526

527
    /** only included for testing purposes */
528
    public static void setTraceStream(OutputStream traceStream) {
529
        TRACE_WRITER = new PrintWriter(traceStream, true);
1✔
530
    }
1✔
531
}
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