1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
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
62 private static final Duration DEFAULT_SOCKET_TIMEOUT = Duration.ofSeconds(60);
63
64
65 private static final String FILE_NAME = "coveralls.json";
66
67
68 private static final String USER_AGENT_STRING = "coveralls-maven-plugin";
69
70
71 private final String coverallsUrl;
72
73
74 private final HttpClient httpClient;
75
76
77 private final ObjectMapper objectMapper;
78
79
80
81
82
83
84
85
86
87
88
89 public CoverallsClient(final String coverallsUrl) {
90 this(coverallsUrl, new HttpClientFactory(coverallsUrl).create(), new ObjectMapper());
91 }
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129 public CoverallsResponse submit(final File file) throws ProcessingException, IOException, InterruptedException {
130 final var filePath = file.toPath();
131
132
133 final String boundary = "----CoverallsBoundary" + System.currentTimeMillis();
134 final String CRLF = "\r\n";
135
136
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
160
161
162
163
164
165
166
167
168
169
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
190
191
192
193
194
195
196
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 }