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

jreleaser / jreleaser / #551

01 Nov 2025 04:25PM UTC coverage: 48.251% (+0.3%) from 47.949%
#551

push

github

aalmiray
build: Update jdks to Java 25

26015 of 53916 relevant lines covered (48.25%)

0.48 hits per line

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

21.12
/sdks/jreleaser-java-sdk-commons/src/main/java/org/jreleaser/sdk/commons/ClientUtils.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 *
4
 * Copyright 2020-2025 The JReleaser authors.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     https://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
package org.jreleaser.sdk.commons;
19

20
import com.fasterxml.jackson.core.JsonProcessingException;
21
import com.fasterxml.jackson.databind.ObjectMapper;
22
import feign.Client;
23
import feign.Feign;
24
import feign.RedirectionInterceptor;
25
import feign.Request;
26
import feign.Response;
27
import feign.form.FormEncoder;
28
import feign.jackson.JacksonDecoder;
29
import feign.jackson.JacksonEncoder;
30
import org.apache.commons.io.IOUtils;
31
import org.apache.tika.Tika;
32
import org.apache.tika.mime.MediaType;
33
import org.jreleaser.bundle.RB;
34
import org.jreleaser.logging.JReleaserLogger;
35
import org.jreleaser.model.Constants;
36
import org.jreleaser.model.JReleaserVersion;
37
import org.jreleaser.model.api.JReleaserContext;
38
import org.jreleaser.model.internal.JReleaserModelPrinter;
39
import org.jreleaser.model.spi.announce.AnnounceException;
40
import org.jreleaser.model.spi.upload.UploadException;
41
import org.jreleaser.sdk.commons.feign.FeignLogger;
42

43
import javax.net.ssl.HostnameVerifier;
44
import javax.net.ssl.SSLContext;
45
import javax.net.ssl.SSLSession;
46
import javax.net.ssl.SSLSocketFactory;
47
import javax.net.ssl.TrustManager;
48
import javax.net.ssl.X509TrustManager;
49
import java.io.IOException;
50
import java.io.OutputStream;
51
import java.io.Reader;
52
import java.net.HttpURLConnection;
53
import java.net.URI;
54
import java.net.URISyntaxException;
55
import java.net.URL;
56
import java.nio.file.Files;
57
import java.nio.file.Path;
58
import java.security.cert.X509Certificate;
59
import java.util.Collection;
60
import java.util.Map;
61
import java.util.UUID;
62
import java.util.concurrent.TimeUnit;
63

64
import static java.nio.charset.StandardCharsets.UTF_8;
65
import static java.util.Collections.emptyMap;
66
import static java.util.Objects.requireNonNull;
67
import static org.jreleaser.util.IoUtils.newInputStreamReader;
68
import static org.jreleaser.util.StringUtils.isNotBlank;
69

70
/**
71
 * @author Andres Almiray
72
 * @since 0.2.0
73
 */
74
@org.jreleaser.infra.nativeimage.annotations.NativeImage
75
public final class ClientUtils {
76
    private static final Tika TIKA = new Tika();
1✔
77
    public static final String CRLF = "\r\n";
78

79
    private ClientUtils() {
80
        // noop
81
    }
82

83
    public static feign.form.FormData toFormData(String fileName, String contentType, String content) {
84
        return toFormData(fileName, contentType, content.getBytes(UTF_8));
×
85
    }
86

87
    public static feign.form.FormData toFormData(String fileName, String contentType, byte[] content) {
88
        return feign.form.FormData.builder()
×
89
            .fileName(fileName)
×
90
            .contentType(contentType)
×
91
            .data(content)
×
92
            .build();
×
93
    }
94

95
    public static feign.form.FormData toFormData(Path asset) throws IOException {
96
        return toFormData(asset.getFileName().toString(),
×
97
            MediaType.parse(TIKA.detect(asset)).toString(),
×
98
            Files.readAllBytes(asset));
×
99
    }
100

101
    public static Feign.Builder builder(JReleaserContext context,
102
                                        int connectTimeout,
103
                                        int readTimeout) {
104
        requireNonNull(context, "'logger' must not be null");
1✔
105

106
        Feign.Builder builder = Feign.builder();
1✔
107

108
        if (Boolean.getBoolean("jreleaser.disableSslValidation")) {
1✔
109
            context.getLogger().warn(RB.$("warn_ssl_disabled"));
×
110
            builder = builder.client(
×
111
                new Client.Default(nonValidatingSSLSocketFactory(),
×
112
                    new NonValidatingHostnameVerifier()));
113
        }
114

115
        return builder
1✔
116
            .logger(new FeignLogger(context.getLogger()))
1✔
117
            .logLevel(FeignLogger.resolveLevel(context))
1✔
118
            .encoder(new FormEncoder(new JacksonEncoder()))
1✔
119
            .decoder(new JacksonDecoder())
1✔
120
            .responseInterceptor(new RedirectionInterceptor())
1✔
121
            .requestInterceptor(template -> template.header("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion()))
1✔
122
            .errorDecoder((methodKey, response) -> new RestAPIException(response.request(), response.status(), response.reason(), toString(context.getLogger(), response.body()), response.headers()))
1✔
123
            .options(new Request.Options(connectTimeout, TimeUnit.SECONDS, readTimeout, TimeUnit.SECONDS, true));
1✔
124
    }
125

126
    private static String toString(JReleaserLogger logger, Response.Body body) {
127
        if (null == body) return "";
1✔
128

129
        try (Reader reader = body.asReader(UTF_8)) {
1✔
130
            return IOUtils.toString(reader);
1✔
131
        } catch (IOException e) {
×
132
            logger.trace(e);
×
133
            return "";
×
134
        }
135
    }
136

137
    public static void webhook(JReleaserLogger logger,
138
                               String webhookUrl,
139
                               int connectTimeout,
140
                               int readTimeout,
141
                               Object message) throws AnnounceException {
142
        if (message instanceof String) {
×
143
            webhook(logger, webhookUrl, connectTimeout, readTimeout, (String) message);
×
144
        }
145

146
        try {
147
            ObjectMapper objectMapper = new ObjectMapper();
×
148
            webhook(logger, webhookUrl, connectTimeout, readTimeout, objectMapper.writeValueAsString(message));
×
149
        } catch (JsonProcessingException e) {
×
150
            throw new AnnounceException(e);
×
151
        }
×
152
    }
×
153

154
    public static void webhook(JReleaserLogger logger,
155
                               String webhookUrl,
156
                               int connectTimeout,
157
                               int readTimeout,
158
                               String message) throws AnnounceException {
159
        post(logger, webhookUrl, connectTimeout, readTimeout, message, emptyMap());
×
160
    }
×
161

162
    public static void post(JReleaserLogger logger,
163
                            String theUrl,
164
                            int connectTimeout,
165
                            int readTimeout,
166
                            String message,
167
                            Map<String, String> headers) throws AnnounceException {
168
        try {
169
            // create URL
170
            URL url = new URI(theUrl).toURL();
1✔
171
            // open connection
172
            logger.debug(RB.$("webhook.connection.open"));
1✔
173
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1✔
174
            // set options
175
            logger.debug(RB.$("webhook.connection.configure"));
1✔
176
            connection.setConnectTimeout(connectTimeout * 1000);
1✔
177
            connection.setReadTimeout(readTimeout * 1000);
1✔
178
            connection.setAllowUserInteraction(false);
1✔
179
            connection.setInstanceFollowRedirects(true);
1✔
180

181
            connection.setRequestMethod("POST");
1✔
182
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
1✔
183
            connection.addRequestProperty("Content-Type", "application/json");
1✔
184
            connection.addRequestProperty("Accept", "application/json");
1✔
185
            headers.forEach(connection::addRequestProperty);
1✔
186
            connection.setDoOutput(true);
1✔
187

188
            // write message
189
            logger.debug(RB.$("webhook.message.send"));
1✔
190
            try (OutputStream os = connection.getOutputStream()) {
1✔
191
                byte[] input = message.getBytes(UTF_8);
1✔
192
                os.write(input, 0, input.length);
1✔
193
            }
194

195
            // handle response
196
            logger.debug(RB.$("webhook.response.handle"));
1✔
197
            int status = connection.getResponseCode();
1✔
198
            if (status >= 400) {
1✔
199
                String reason = connection.getResponseMessage();
×
200
                StringBuilder b = new StringBuilder("Webhook replied with: ")
×
201
                    .append(status);
×
202
                if (isNotBlank(reason)) {
×
203
                    b.append(" reason: ")
×
204
                        .append(reason);
×
205
                }
206
                try (Reader reader = newInputStreamReader(connection.getErrorStream())) {
×
207
                    message = IOUtils.toString(reader);
×
208
                    if (isNotBlank(message)) {
×
209
                        b.append(",")
×
210
                            .append(message);
×
211
                    }
212
                }
213
                throw new AnnounceException(b.toString());
×
214
            }
215
        } catch (URISyntaxException | IOException e) {
×
216
            logger.trace(e);
×
217
            throw new AnnounceException(e);
×
218
        }
1✔
219
    }
1✔
220

221
    public static Reader postFile(JReleaserLogger logger,
222
                                  URI uri,
223
                                  int connectTimeout,
224
                                  int readTimeout,
225
                                  feign.form.FormData data,
226
                                  Map<String, String> headers) throws UploadException {
227
        headers.put("METHOD", "POST");
×
228
        return uploadFile(logger, uri, connectTimeout, readTimeout, data, headers);
×
229
    }
230

231
    public static Reader postFile(JReleaserLogger logger,
232
                                  String url,
233
                                  int connectTimeout,
234
                                  int readTimeout,
235
                                  feign.form.FormData data,
236
                                  Map<String, String> headers) throws UploadException {
237
        headers.put("METHOD", "POST");
×
238
        try {
239
            return uploadFile(logger, new URI(url), connectTimeout, readTimeout, data, headers);
×
240
        } catch (URISyntaxException e) {
×
241
            logger.trace(e);
×
242
            throw new UploadException(e);
×
243
        }
244
    }
245

246
    public static Reader putFile(JReleaserLogger logger,
247
                                 String url,
248
                                 int connectTimeout,
249
                                 int readTimeout,
250
                                 feign.form.FormData data,
251
                                 Map<String, String> headers) throws UploadException {
252
        headers.put("METHOD", "PUT");
×
253
        headers.put("Expect", "100-continue");
×
254
        try {
255
            return uploadFile(logger, new URI(url), connectTimeout, readTimeout, data, headers);
×
256
        } catch (URISyntaxException e) {
×
257
            logger.trace(e);
×
258
            throw new UploadException(e);
×
259
        }
260
    }
261

262
    private static Reader uploadFile(JReleaserLogger logger,
263
                                     URI uri,
264
                                     int connectTimeout,
265
                                     int readTimeout,
266
                                     feign.form.FormData data,
267
                                     Map<String, String> headers) throws UploadException {
268
        try {
269
            // create URL
270
            URL theUrl = uri.toURL();
×
271
            logger.debug("url: {}", theUrl);
×
272

273
            // open connection
274
            logger.debug(RB.$("webhook.connection.open"));
×
275
            HttpURLConnection connection = (HttpURLConnection) theUrl.openConnection();
×
276
            // set options
277
            logger.debug(RB.$("webhook.connection.configure"));
×
278
            connection.setConnectTimeout(connectTimeout * 1000);
×
279
            connection.setReadTimeout(readTimeout * 1000);
×
280
            connection.setAllowUserInteraction(false);
×
281
            connection.setInstanceFollowRedirects(true);
×
282

283
            connection.setRequestMethod(headers.remove("METHOD"));
×
284
            if (!headers.containsKey("Accept")) {
×
285
                connection.addRequestProperty("Accept", "*/*");
×
286
            }
287
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
×
288
            connection.addRequestProperty("Content-Length", data.getData().length + "");
×
289
            connection.setRequestProperty("Content-Type", data.getContentType());
×
290
            headers.forEach(connection::setRequestProperty);
×
291

292
            connection.getRequestProperties().forEach((k, v) -> {
×
293
                if (JReleaserModelPrinter.isSecret(k)) {
×
294
                    logger.debug("{}: {}", k, Constants.HIDE);
×
295
                } else {
296
                    logger.debug("{}: {}", k, v);
×
297
                }
298
            });
×
299

300
            connection.setDoOutput(true);
×
301

302
            // write message
303
            logger.debug(RB.$("webhook.data.send"));
×
304
            try (OutputStream os = connection.getOutputStream()) {
×
305
                os.write(data.getData(), 0, data.getData().length);
×
306
                os.flush();
×
307
            }
308

309
            // handle response
310
            logger.debug(RB.$("webhook.response.handle"));
×
311
            int status = connection.getResponseCode();
×
312
            if (status >= 400) {
×
313
                String reason = connection.getResponseMessage();
×
314
                StringBuilder b = new StringBuilder("Got ")
×
315
                    .append(status);
×
316
                if (isNotBlank(reason)) {
×
317
                    b.append(" reason: ")
×
318
                        .append(reason);
×
319
                }
320
                logger.trace(RB.$("webhook.server.reply", status, reason));
×
321

322
                try (Reader reader = newInputStreamReader(connection.getErrorStream())) {
×
323
                    String message = IOUtils.toString(reader);
×
324
                    if (isNotBlank(message)) {
×
325
                        b.append(", ")
×
326
                            .append(message);
×
327
                    }
328
                }
329
                throw new UploadException(b.toString());
×
330
            }
331

332
            return newInputStreamReader(connection.getInputStream());
×
333
        } catch (IOException e) {
×
334
            logger.trace(e);
×
335
            throw new UploadException(e);
×
336
        }
337
    }
338

339
    public static Reader postData(JReleaserLogger logger,
340
                                  String url,
341
                                  int connectTimeout,
342
                                  int readTimeout,
343
                                  Collection<FormData> data,
344
                                  Map<String, String> headers) throws UploadException {
345
        headers.put("METHOD", "POST");
×
346
        try {
347
            return upload(logger, new URI(url), connectTimeout, readTimeout, data, headers);
×
348
        } catch (URISyntaxException e) {
×
349
            logger.trace(e);
×
350
            throw new UploadException(e);
×
351
        }
352
    }
353

354
    private static Reader upload(JReleaserLogger logger,
355
                                 URI uri,
356
                                 int connectTimeout,
357
                                 int readTimeout,
358
                                 Collection<FormData> data,
359
                                 Map<String, String> headers) throws UploadException {
360
        try {
361
            // create URL
362
            URL theUrl = uri.toURL();
×
363
            logger.debug("url: {}", theUrl);
×
364

365
            // open connection
366
            logger.debug(RB.$("webhook.connection.open"));
×
367
            HttpURLConnection connection = (HttpURLConnection) theUrl.openConnection();
×
368
            // set options
369
            logger.debug(RB.$("webhook.connection.configure"));
×
370
            connection.setConnectTimeout(connectTimeout * 1000);
×
371
            connection.setReadTimeout(readTimeout * 1000);
×
372
            connection.setAllowUserInteraction(false);
×
373
            connection.setInstanceFollowRedirects(true);
×
374

375
            connection.setRequestMethod(headers.remove("METHOD"));
×
376
            if (!headers.containsKey("Accept")) {
×
377
                connection.addRequestProperty("Accept", "*/*");
×
378
            }
379
            String boundary = UUID.randomUUID().toString();
×
380
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
×
381
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
×
382
            headers.forEach(connection::setRequestProperty);
×
383

384
            connection.getRequestProperties().forEach((k, v) -> {
×
385
                if (JReleaserModelPrinter.isSecret(k)) {
×
386
                    logger.debug("{}: {}", k, Constants.HIDE);
×
387
                } else {
388
                    logger.debug("{}: {}", k, v);
×
389
                }
390
            });
×
391

392
            connection.setDoOutput(true);
×
393

394
            // write message
395
            logger.debug(RB.$("webhook.data.send"));
×
396
            try (OutputStream os = connection.getOutputStream()) {
×
397
                byte[] boundaryData = (CRLF + "--" + boundary + CRLF).getBytes(UTF_8);
×
398
                byte[] boundaryDataEnd = (CRLF + "--" + boundary + "--" + CRLF).getBytes(UTF_8);
×
399
                os.write(boundaryData, 0, boundaryData.length);
×
400

401
                int i = 1;
×
402
                for (FormData d : data) {
×
403
                    d.writeTo(os);
×
404
                    if (i++ != data.size()) {
×
405
                        os.write(boundaryData, 0, boundaryData.length);
×
406
                    } else {
407
                        os.write(boundaryDataEnd, 0, boundaryDataEnd.length);
×
408
                    }
409
                }
×
410
            }
411

412
            // handle response
413
            logger.debug(RB.$("webhook.response.handle"));
×
414
            int status = connection.getResponseCode();
×
415
            if (status >= 400) {
×
416
                String reason = connection.getResponseMessage();
×
417
                StringBuilder b = new StringBuilder("Got ")
×
418
                    .append(status);
×
419
                if (isNotBlank(reason)) {
×
420
                    b.append(" reason: ")
×
421
                        .append(reason);
×
422
                }
423
                logger.trace(RB.$("webhook.server.reply", status, reason));
×
424

425
                try (Reader reader = newInputStreamReader(connection.getErrorStream())) {
×
426
                    String message = IOUtils.toString(reader);
×
427
                    if (isNotBlank(message)) {
×
428
                        b.append(", ")
×
429
                            .append(message);
×
430
                    }
431
                }
432
                throw new UploadException(b.toString());
×
433
            }
434

435
            return newInputStreamReader(connection.getInputStream());
×
436
        } catch (IOException e) {
×
437
            logger.trace(e);
×
438
            throw new UploadException(e);
×
439
        }
440
    }
441

442
    public static boolean head(JReleaserLogger logger,
443
                               String theUrl,
444
                               int connectTimeout,
445
                               int readTimeout) throws RestAPIException {
446
        try {
447
            // create URL
448
            URL url = new URI(theUrl).toURL();
1✔
449
            // open connection
450
            logger.debug(RB.$("webhook.connection.open"));
1✔
451
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1✔
452
            // set options
453
            logger.debug(RB.$("webhook.connection.configure"));
1✔
454
            connection.setConnectTimeout(connectTimeout * 1000);
1✔
455
            connection.setReadTimeout(readTimeout * 1000);
1✔
456
            connection.setAllowUserInteraction(false);
1✔
457
            connection.setInstanceFollowRedirects(true);
1✔
458

459
            connection.setRequestMethod("HEAD");
1✔
460
            connection.addRequestProperty("User-Agent", "JReleaser/" + JReleaserVersion.getPlainVersion());
1✔
461

462
            // handle response
463
            logger.debug(RB.$("webhook.response.handle"));
1✔
464
            int status = connection.getResponseCode();
1✔
465
            if (status == 200) return true;
1✔
466
            if (status == 404) return false;
1✔
467

468
            String reason = connection.getResponseMessage();
×
469
            StringBuilder b = new StringBuilder("Request replied with: ")
×
470
                .append(status);
×
471
            if (isNotBlank(reason)) {
×
472
                b.append(" reason: ")
×
473
                    .append(reason);
×
474
            }
475
            throw new RestAPIException(status, b.toString());
×
476
        } catch (URISyntaxException | IOException e) {
×
477
            logger.trace(e);
×
478
            throw new RestAPIException(500, e);
×
479
        }
480
    }
481

482
    private static SSLSocketFactory nonValidatingSSLSocketFactory() {
483
        try {
484
            SSLContext sslContext = SSLContext.getInstance("SSL");
×
485
            sslContext.init(null, new TrustManager[]{new NonValidatingTrustManager()}, null); // lgtm [java/insecure-trustmanager]
×
486
            return sslContext.getSocketFactory();
×
487
        } catch (Exception e) {
×
488
            throw new IllegalStateException(e);
×
489
        }
490
    }
491

492
    private static class NonValidatingTrustManager implements X509TrustManager {
493
        private static final X509Certificate[] EMPTY_CERTIFICATES = new X509Certificate[0];
×
494

495
        @Override
496
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
497
            // noop
498
        }
×
499

500
        @Override
501
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
502
            // noop
503
        }
×
504

505
        @Override
506
        public X509Certificate[] getAcceptedIssuers() {
507
            return EMPTY_CERTIFICATES;
×
508
        }
509
    }
510

511
    private static class NonValidatingHostnameVerifier implements HostnameVerifier {
512
        @Override
513
        public boolean verify(String hostname, SSLSession session) {
514
            return true;
×
515
        }
516
    }
517

518
    public interface FormData {
519
        void writeTo(OutputStream os) throws IOException;
520
    }
521

522
    public static class FieldFormData implements FormData {
523
        private final String name;
524
        private final Object value;
525

526
        public FieldFormData(String name, Object value) {
×
527
            this.name = name;
×
528
            this.value = value;
×
529
        }
×
530

531
        @Override
532
        public void writeTo(OutputStream os) throws IOException {
533
            String header = "Content-Disposition: form-data; name=\"" + name + "\"" + CRLF + CRLF;
×
534
            byte[] input = header.getBytes(UTF_8);
×
535
            os.write(input, 0, input.length);
×
536
            byte[] data = String.valueOf(value).getBytes(UTF_8);
×
537
            os.write(data, 0, data.length);
×
538
        }
×
539
    }
540

541
    public static class FileFormData implements FormData {
542
        private final String name;
543
        private final Path path;
544

545
        public FileFormData(String name, Path path) {
×
546
            this.name = name;
×
547
            this.path = path;
×
548
        }
×
549

550
        @Override
551
        public void writeTo(OutputStream os) throws IOException {
552
            byte[] data = Files.readAllBytes(path);
×
553
            String header = "Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + path.getFileName() + "\"" + CRLF
×
554
                + "Content-Type: application/octet-stream" + CRLF + CRLF;
555
            byte[] input = header.getBytes(UTF_8);
×
556
            os.write(input, 0, input.length);
×
557
            os.write(data, 0, data.length);
×
558
        }
×
559
    }
560
}
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