View Javadoc
1   /*
2    * The MIT License (MIT)
3    *
4    * Copyright (c) 2013-2026 The Coveralls Maven Plugin Project Contributors:
5    *     https://github.com/hazendaz/coveralls-maven-plugin/graphs/contributors
6    *
7    * Permission is hereby granted, free of charge, to any person obtaining a copy
8    * of this software and associated documentation files (the "Software"), to deal
9    * in the Software without restriction, including without limitation the rights
10   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11   * copies of the Software, and to permit persons to whom the Software is
12   * furnished to do so, subject to the following conditions:
13   *
14   * The above copyright notice and this permission notice shall be included in
15   * all copies or substantial portions of the Software.
16   *
17   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23   * THE SOFTWARE.
24   */
25  package org.eluder.coveralls.maven.plugin.httpclient;
26  
27  import com.fasterxml.jackson.databind.ObjectMapper;
28  
29  import java.io.BufferedReader;
30  import java.io.ByteArrayOutputStream;
31  import java.io.File;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.InputStreamReader;
35  import java.net.URI;
36  import java.net.http.HttpClient;
37  import java.net.http.HttpRequest;
38  import java.net.http.HttpResponse;
39  import java.nio.charset.StandardCharsets;
40  import java.nio.file.Files;
41  import java.security.Provider;
42  import java.security.Security;
43  import java.time.Duration;
44  
45  import org.eluder.coveralls.maven.plugin.ProcessingException;
46  import org.eluder.coveralls.maven.plugin.domain.CoverallsResponse;
47  
48  /**
49   * The Class CoverallsClient.
50   */
51  public class CoverallsClient {
52  
53      static {
54          for (final Provider provider : Security.getProviders()) {
55              if (provider.getName().startsWith("SunPKCS11")) {
56                  Security.removeProvider(provider.getName());
57              }
58          }
59      }
60  
61      /** The Constant DEFAULT_SOCKET_TIMEOUT. */
62      private static final Duration DEFAULT_SOCKET_TIMEOUT = Duration.ofSeconds(60);
63  
64      /** The Constant FILE_NAME. */
65      private static final String FILE_NAME = "coveralls.json";
66  
67      /** The Constant USER_AGENT_STRING. */
68      private static final String USER_AGENT_STRING = "coveralls-maven-plugin";
69  
70      /** The coveralls url. */
71      private final String coverallsUrl;
72  
73      /** The http client. */
74      private final HttpClient httpClient;
75  
76      /** The object mapper. */
77      private final ObjectMapper objectMapper;
78  
79      /**
80       * Instantiates a new Coveralls Client.
81       *
82       * @param coverallsUrl
83       *            The base url for the Coveralls API. This should generally be set to
84       *
85       *            <pre>
86       * https://coveralls.io/api/v1/jobs
87       *            </pre>
88       */
89      public CoverallsClient(final String coverallsUrl) {
90          this(coverallsUrl, new HttpClientFactory(coverallsUrl).create(), new ObjectMapper());
91      }
92  
93      /**
94       * Instantiates a new Coveralls Client.
95       *
96       * @param coverallsUrl
97       *            The base url for the Coveralls API. This should generally be set to
98       *
99       *            <pre>
100      * https://coveralls.io/api/v1/jobs
101      *            </pre>
102      *
103      * @param httpClient
104      *            An implementation of {@link HttpClient}
105      * @param objectMapper
106      *            A Jackson {@link ObjectMapper}
107      */
108     public CoverallsClient(final String coverallsUrl, final HttpClient httpClient, final ObjectMapper objectMapper) {
109         this.coverallsUrl = coverallsUrl;
110         this.httpClient = httpClient;
111         this.objectMapper = objectMapper;
112     }
113 
114     /**
115      * Submit a coveralls json file to the API.
116      *
117      * @param file
118      *            A coveralls report that can be submitted to the jobs API
119      *
120      * @return An API response body deserialized to a {@link CoverallsResponse}
121      *
122      * @throws ProcessingException
123      *             the processing exception
124      * @throws IOException
125      *             Signals that an I/O exception has occurred.
126      * @throws InterruptedException
127      *             the interrupted exception
128      */
129     public CoverallsResponse submit(final File file) throws ProcessingException, IOException, InterruptedException {
130         final var filePath = file.toPath();
131 
132         // Generate a unique boundary
133         final String boundary = "----CoverallsBoundary" + System.currentTimeMillis();
134         final String CRLF = "\r\n";
135 
136         // Build multipart body
137         final var byteStream = new ByteArrayOutputStream();
138         byteStream.write(("--" + boundary + CRLF).getBytes(StandardCharsets.UTF_8));
139         byteStream.write(("Content-Disposition: form-data; name=\"json_file\"; filename=\"" + FILE_NAME + "\"" + CRLF)
140                 .getBytes(StandardCharsets.UTF_8));
141         byteStream
142                 .write(("Content-Type: application/json;charset=UTF-8" + CRLF + CRLF).getBytes(StandardCharsets.UTF_8));
143         byteStream.write(Files.readAllBytes(filePath));
144         byteStream.write((CRLF + "--" + boundary + "--" + CRLF).getBytes(StandardCharsets.UTF_8));
145         final byte[] multipartBody = byteStream.toByteArray();
146 
147         final var request = HttpRequest.newBuilder().version(HttpClient.Version.HTTP_1_1)
148                 .uri(URI.create(this.coverallsUrl)).timeout(CoverallsClient.DEFAULT_SOCKET_TIMEOUT)
149                 .header("User-Agent", CoverallsClient.USER_AGENT_STRING)
150                 .header("Content-Type", "multipart/form-data; boundary=" + boundary)
151                 .POST(HttpRequest.BodyPublishers.ofByteArray(multipartBody)).build();
152 
153         final HttpResponse<InputStream> response = this.httpClient.send(request,
154                 HttpResponse.BodyHandlers.ofInputStream());
155         return this.parseResponse(response);
156     }
157 
158     /**
159      * Parses the response.
160      *
161      * @param response
162      *            the response
163      *
164      * @return the coveralls response
165      *
166      * @throws ProcessingException
167      *             the processing exception
168      * @throws IOException
169      *             Signals that an I/O exception has occurred.
170      */
171     private CoverallsResponse parseResponse(final HttpResponse<InputStream> response)
172             throws ProcessingException, IOException {
173         if (response.statusCode() >= 500) {
174             throw new IOException(this.getResponseErrorMessage(response, "Coveralls API internal error"));
175         }
176 
177         try (var reader = new BufferedReader(new InputStreamReader(response.body(), StandardCharsets.UTF_8))) {
178             final var cr = this.objectMapper.readValue(reader, CoverallsResponse.class);
179             if (cr.isError()) {
180                 throw new ProcessingException(this.getResponseErrorMessage(response, cr.getMessage()));
181             }
182             return cr;
183         } catch (final IOException e) {
184             throw new ProcessingException(this.getResponseErrorMessage(response, e.getMessage()), e);
185         }
186     }
187 
188     /**
189      * Gets the response error message.
190      *
191      * @param response
192      *            the response
193      * @param message
194      *            the message
195      *
196      * @return the response error message
197      */
198     private String getResponseErrorMessage(final HttpResponse<InputStream> response, final String message) {
199         final var errorMessage = new StringBuilder("Report submission to Coveralls API failed with HTTP status ")
200                 .append(response.statusCode());
201         if (message != null && !message.isBlank()) {
202             errorMessage.append(": ").append(message);
203         }
204         return errorMessage.toString();
205     }
206 }