• 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

71.18
/sdks/jreleaser-java-sdk-commons/src/main/java/org/jreleaser/sdk/commons/AbstractMavenDeployer.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 feign.form.FormData;
21
import org.apache.commons.io.IOUtils;
22
import org.jreleaser.bundle.RB;
23
import org.jreleaser.model.JReleaserException;
24
import org.jreleaser.model.Signing;
25
import org.jreleaser.model.api.signing.SigningException;
26
import org.jreleaser.model.internal.JReleaserContext;
27
import org.jreleaser.model.internal.deploy.maven.Maven;
28
import org.jreleaser.model.spi.deploy.DeployException;
29
import org.jreleaser.model.spi.deploy.maven.Deployable;
30
import org.jreleaser.model.spi.deploy.maven.MavenDeployer;
31
import org.jreleaser.model.spi.upload.UploadException;
32
import org.jreleaser.sdk.command.Command;
33
import org.jreleaser.sdk.command.CommandException;
34
import org.jreleaser.sdk.signing.SigningUtils;
35
import org.jreleaser.sdk.tool.PomChecker;
36
import org.jreleaser.sdk.tool.ToolException;
37
import org.jreleaser.util.Algorithm;
38
import org.jreleaser.util.ChecksumUtils;
39
import org.jreleaser.util.CollectionUtils;
40
import org.jreleaser.util.Errors;
41
import org.jreleaser.util.FileUtils;
42
import org.w3c.dom.Document;
43
import org.xml.sax.SAXException;
44

45
import javax.xml.parsers.DocumentBuilderFactory;
46
import javax.xml.parsers.ParserConfigurationException;
47
import javax.xml.xpath.XPathConstants;
48
import javax.xml.xpath.XPathExpressionException;
49
import javax.xml.xpath.XPathFactory;
50
import java.io.IOException;
51
import java.net.HttpURLConnection;
52
import java.net.MalformedURLException;
53
import java.net.URI;
54
import java.net.URISyntaxException;
55
import java.net.URL;
56
import java.nio.file.FileVisitResult;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.nio.file.SimpleFileVisitor;
60
import java.nio.file.attribute.BasicFileAttributes;
61
import java.nio.file.attribute.FileTime;
62
import java.time.Instant;
63
import java.time.LocalDateTime;
64
import java.time.ZoneId;
65
import java.util.ArrayList;
66
import java.util.Enumeration;
67
import java.util.LinkedHashMap;
68
import java.util.LinkedHashSet;
69
import java.util.List;
70
import java.util.Locale;
71
import java.util.Map;
72
import java.util.Optional;
73
import java.util.Set;
74
import java.util.TreeSet;
75
import java.util.function.Function;
76
import java.util.jar.JarEntry;
77
import java.util.jar.JarFile;
78
import java.util.stream.Collectors;
79

80
import static java.nio.charset.StandardCharsets.UTF_8;
81
import static java.nio.file.FileVisitResult.CONTINUE;
82
import static java.util.stream.Collectors.toSet;
83
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_ASC;
84
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_JAR;
85
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_POM;
86
import static org.jreleaser.model.spi.deploy.maven.Deployable.EXT_WAR;
87
import static org.jreleaser.model.spi.deploy.maven.Deployable.MAVEN_METADATA_XML;
88
import static org.jreleaser.model.spi.deploy.maven.Deployable.PACKAGING_JAR;
89
import static org.jreleaser.model.spi.deploy.maven.Deployable.PACKAGING_WAR;
90
import static org.jreleaser.util.StringUtils.isNotBlank;
91

92
/**
93
 * @author Andres Almiray
94
 * @since 1.4.0
95
 */
96
public abstract class AbstractMavenDeployer<A extends org.jreleaser.model.api.deploy.maven.MavenDeployer,
97
    D extends org.jreleaser.model.internal.deploy.maven.MavenDeployer<A>> implements MavenDeployer<A, D> {
98
    private static final Algorithm[] ALGORITHMS = {
1✔
99
        Algorithm.MD5,
100
        Algorithm.SHA_1,
101
        Algorithm.SHA_256,
102
        Algorithm.SHA_512
103
    };
104

105
    private static final String BUILD_TAG = "-build";
106
    private static final Map<String, String> KEY_SERVERS = CollectionUtils.<String, String>map()
1✔
107
        .e("https://keys.openpgp.org", "https://keys.openpgp.org/search?q=%s")
1✔
108
        .e("https://keyserver.ubuntu.com", "https://keyserver.ubuntu.com/pks/lookup?search=%s&fingerprint=on&options=mr&op=index")
1✔
109
        .e("https://pgp.mit.edu", "https://pgp.mit.edu/pks/lookup?op=get&search=0x%s");
1✔
110

111
    protected final JReleaserContext context;
112

113
    protected AbstractMavenDeployer(JReleaserContext context) {
1✔
114
        this.context = context;
1✔
115
    }
1✔
116

117
    protected Set<Deployable> collectDeployableArtifacts() {
118
        Set<Deployable> deployables = new TreeSet<>();
1✔
119
        for (String stagingRepository : getDeployer().getStagingRepositories()) {
1✔
120
            collectDeployables(deployables, stagingRepository);
1✔
121
        }
1✔
122

123
        return deployables;
1✔
124
    }
125

126
    protected Set<Deployable> collectDeployables() {
127
        return collectDeployables(true);
1✔
128
    }
129

130
    protected Set<Deployable> collectDeployables(boolean checksum) {
131
        Set<Deployable> deployables = collectDeployableArtifacts().stream()
1✔
132
            .filter(deployable -> checksum || !deployable.isChecksum())
1✔
133
            .collect(toSet());
1✔
134

135
        Map<String, Deployable> deployablesMap = deployables.stream()
1✔
136
            .collect(Collectors.toMap(Deployable::getFullDeployPath, Function.identity()));
1✔
137

138
        Errors errors = checkMavenCentralRules(deployablesMap);
1✔
139
        if (errors.hasErrors()) {
1✔
140
            errors.logErrors(context.getLogger());
×
141
            throw new JReleaserException(RB.$("ERROR_deployer_maven_central_rules"));
×
142
        }
143

144
        signDeployables(deployablesMap, deployables);
1✔
145
        if (checksum) checksumDeployables(deployablesMap, deployables);
1✔
146

147
        return deployables;
1✔
148
    }
149

150
    public Set<Deployable> collectDeployables(Set<Deployable> deployables, String stagingRepository) {
151
        Path root = context.getBasedir().resolve(stagingRepository).normalize();
1✔
152

153
        if (!Files.exists(root)) {
1✔
154
            throw new JReleaserException(RB.$("validation_directory_not_exist",
×
155
                "maven." + getDeployer().getType() + "." + getDeployer().getName() + ".stagingRepository",
×
156
                context.relativizeToBasedir(root).toString()));
×
157
        }
158

159
        if (!root.toFile().isDirectory()) {
1✔
160
            throw new JReleaserException(RB.$("validation_is_not_a_directory",
×
161
                "maven." + getDeployer().getType() + "." + getDeployer().getName() + ".stagingRepository",
×
162
                context.relativizeToBasedir(root).toString()));
×
163
        }
164

165
        try {
166
            DeployableCollector collector = new DeployableCollector(root, context.getModel().getProject().isSnapshot());
1✔
167

168
            java.nio.file.Files.walkFileTree(root, collector);
1✔
169
            if (collector.failed) {
1✔
170
                throw new JReleaserException(RB.$("ERROR_deployer_stage_resolution"));
×
171
            }
172

173
            deployables.addAll(collector.deployables);
1✔
174
        } catch (IOException e) {
×
175
            throw new JReleaserException(RB.$("ERROR_deployer_unexpected_error_stage"), e);
×
176
        }
1✔
177

178
        return deployables;
1✔
179
    }
180

181
    private Errors checkMavenCentralRules(Map<String, Deployable> deployablesMap) {
182
        Errors errors = new Errors();
1✔
183

184
        context.getLogger().info(RB.$("deployers.maven.prerequisites"));
1✔
185

186
        Map<String, Deployable> buildPoms = new LinkedHashMap<>();
1✔
187

188
        // 1st check jar, sources, javadoc if applicable
189
        for (Deployable deployable : deployablesMap.values()) {
1✔
190
            if (!deployable.getFilename().endsWith(EXT_POM)) {
1✔
191
                continue;
1✔
192
            }
193

194
            String base = deployable.getFilename();
1✔
195
            base = base.substring(0, base.length() - 4);
1✔
196

197
            boolean buildPom = false;
1✔
198
            if (base.endsWith(BUILD_TAG)) {
1✔
199
                Deployable baseDeployable = deployable.deriveByFilename(base.substring(0, base.length() - BUILD_TAG.length()) + EXT_POM);
×
200
                if (deployablesMap.containsKey(baseDeployable.getFullDeployPath())) {
×
201
                    buildPom = true;
×
202
                    buildPoms.put(deployable.getFullDeployPath(), deployable);
×
203
                }
204
            }
205

206
            if (!buildPom) {
1✔
207
                if (requiresJar(deployable)) {
1✔
208
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + EXT_JAR);
1✔
209
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
210
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
211
                    }
212
                }
213

214
                if (!deployable.isRelocated() && deployable.requiresWar()) {
1✔
215
                    Deployable derived = deployable.deriveByFilename(PACKAGING_WAR, base + EXT_WAR);
×
216
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
×
217
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
218
                    }
219
                }
220

221
                if (!deployable.isRelocated() && requiresSourcesJar(deployable, base)) {
1✔
222
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + "-sources.jar");
1✔
223
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
224
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
225
                    }
226
                }
227

228
                if (!deployable.isRelocated() && requiresJavadocJar(deployable, base)) {
1✔
229
                    Deployable derived = deployable.deriveByFilename(PACKAGING_JAR, base + "-javadoc.jar");
1✔
230
                    if (!deployablesMap.containsKey(derived.getFullDeployPath())) {
1✔
231
                        errors.configuration(RB.$("validation_is_missing", derived.getFilename()));
×
232
                    }
233
                }
234
            }
235
        }
1✔
236

237
        if (!getDeployer().isVerifyPom()) {
1✔
238
            return errors;
1✔
239
        }
240

241
        context.getLogger().info(RB.$("deployers.maven.verify.poms"));
1✔
242

243
        Maven.Pomchecker pomcheckerModel = context.getModel().getDeploy().getMaven().getPomchecker();
1✔
244
        PomChecker pomChecker = new PomChecker(context.asImmutable(),
1✔
245
            pomcheckerModel.getVersion());
1✔
246
        try {
247
            if (!pomChecker.setup()) {
1✔
248
                context.getLogger().warn(RB.$("tool_unavailable", "pomchecker"));
×
249
                return errors;
×
250
            }
251
        } catch (ToolException e) {
×
252
            context.getLogger().warn(RB.$("tool_unavailable", "pomchecker"), e);
×
253
            return errors;
×
254
        }
1✔
255

256
        Set<String> paths = new LinkedHashSet<>();
1✔
257
        for (String stagingRepository : getDeployer().getStagingRepositories()) {
1✔
258
            paths.add(context.getBasedir().resolve(stagingRepository)
1✔
259
                .toAbsolutePath()
1✔
260
                .normalize()
1✔
261
                .toString());
1✔
262
        }
1✔
263

264
        // 2nd check pom
265
        for (Deployable deployable : deployablesMap.values()) {
1✔
266
            if (!deployable.getFilename().endsWith(EXT_POM) || buildPoms.containsKey(deployable.getFullDeployPath()) ||
1✔
267
                !requiresPomVerification(deployable)) {
1✔
268
                continue;
×
269
            }
270

271
            List<String> args = new ArrayList<>();
1✔
272
            args.add("check-maven-central");
1✔
273
            args.add("--quiet");
1✔
274
            if (context.getModel().getProject().isSnapshot() &&
1✔
275
                getDeployer().isSnapshotSupported()) {
×
276
                args.add("--no-release");
×
277
            }
278
            if (pomChecker.isVersionCompatibleWith("1.9.0")) {
1✔
279
                if (!pomcheckerModel.isFailOnWarning()) {
1✔
280
                    args.add("--no-fail-on-warning");
×
281
                }
282
                if (!pomcheckerModel.isFailOnError()) {
1✔
283
                    args.add("--no-fail-on-error");
×
284
                }
285
            }
286

287
            if (pomChecker.isVersionCompatibleWith("1.13.0")) {
1✔
288
                for (String path : paths) {
1✔
289
                    args.add("--repository");
1✔
290
                    args.add(path);
1✔
291
                }
1✔
292
            }
293

294
            if (pomcheckerModel.isStrict()) {
1✔
295
                args.add("--strict");
1✔
296
            } else {
297
                args.add("--no-strict");
×
298
            }
299

300
            args.add("--file");
1✔
301
            args.add(deployable.getLocalPath().toAbsolutePath().toString());
1✔
302

303
            context.getLogger().debug(RB.$("deployers.maven.verify.pom", deployable.getLocalPath()));
1✔
304

305
            Command.Result result = Command.Result.empty();
1✔
306
            try {
307
                result = pomChecker.check(context.getBasedir(), args);
1✔
308
            } catch (CommandException e) {
×
309
                handlePomcheckerResult(deployable.getLocalPath().getFileName().toString(), result, e, errors);
×
310
            }
1✔
311

312
            if (result.getExitValue() != 0) {
1✔
313
                handlePomcheckerResult(deployable.getLocalPath().getFileName().toString(), result, null, errors);
×
314
            }
315
        }
1✔
316

317
        return errors;
1✔
318
    }
319

320
    private void handlePomcheckerResult(String filename, Command.Result result, CommandException e, Errors errors) {
321
        String plumbing = result.getErr();
×
322
        String validation = result.getOut();
×
323

324
        // 1st check out -> validation issues
325
        if (isNotBlank(validation)) {
×
326
            errors.configuration(RB.$("ERROR_deployer_pomchecker_header", filename, validation));
×
327
        } else if (isNotBlank(plumbing)) {
×
328
            // 2nd check err -> plumbing issues
329
            errors.configuration(RB.$("ERROR_deployer_pomchecker_header", filename, plumbing));
×
330
        } else if (null != e) {
×
331
            // command failed and we've got no clue!
332
            errors.configuration(e.getMessage());
×
333
        }
334
    }
×
335

336
    private boolean requiresJar(Deployable deployable) {
337
        if (!deployable.requiresJar()) return false;
1✔
338

339
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
340
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
341
            .findFirst();
1✔
342

343
        if (override.isPresent() && (override.get().isJarSet())) return override.get().isJar();
1✔
344

345
        return !deployable.isRelocated();
1✔
346
    }
347

348
    private boolean requiresSourcesJar(Deployable deployable, String baseFilename) {
349
        if (!deployable.requiresSourcesJar()) return false;
1✔
350

351
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
352
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
353
            .findFirst();
1✔
354

355
        if (override.isPresent() && (override.get().isSourceJarSet())) return override.get().isSourceJar();
1✔
356

357
        if (!getDeployer().isSourceJarSet()) {
1✔
358
            return hasJavaClass(deployable, baseFilename);
1✔
359
        }
360

361
        return getDeployer().isSourceJar();
×
362
    }
363

364
    private boolean requiresJavadocJar(Deployable deployable, String baseFilename) {
365
        if (!deployable.requiresJavadocJar()) return false;
1✔
366

367
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
368
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
369
            .findFirst();
1✔
370

371
        if (override.isPresent() && (override.get().isJavadocJarSet())) return override.get().isJavadocJar();
1✔
372

373
        if (!getDeployer().isJavadocJarSet()) {
1✔
374
            return hasJavaClass(deployable, baseFilename);
1✔
375
        }
376

377
        return getDeployer().isJavadocJar();
×
378
    }
379

380
    private boolean hasJavaClass(Deployable deployable, String baseFilename) {
381
        Deployable jar = deployable.deriveByFilename(PACKAGING_JAR, baseFilename + EXT_JAR);
1✔
382
        JarFile jarFile = null;
1✔
383

384
        try {
385
            jarFile = new JarFile(jar.getLocalPath().toFile());
1✔
386
            Enumeration<JarEntry> entries = jarFile.entries();
1✔
387
            while (entries.hasMoreElements()) {
1✔
388
                JarEntry entry = entries.nextElement();
1✔
389
                if (entry.getName().endsWith(".class")) {
1✔
390
                    return true;
1✔
391
                }
392
            }
1✔
393
            return false;
×
394
        } catch (IOException e) {
×
395
            context.getLogger().warn(RB.$("ERROR_deployer_read_local_file", jar.getLocalPath()), e);
×
396
            return false;
×
397
        } finally {
398
            IOUtils.closeQuietly(jarFile);
1✔
399
        }
400
    }
401

402
    private boolean requiresPomVerification(Deployable deployable) {
403
        Optional<org.jreleaser.model.internal.deploy.maven.MavenDeployer.ArtifactOverride> override = getDeployer().getArtifactOverrides().stream()
1✔
404
            .filter(a -> a.getGroupId().equals(deployable.getGroupId()) && a.getArtifactId().equals(deployable.getArtifactId()))
1✔
405
            .findFirst();
1✔
406

407
        if (override.isPresent() && (override.get().isVerifyPomSet())) return override.get().isVerifyPom();
1✔
408

409
        return getDeployer().isVerifyPom();
1✔
410
    }
411

412
    private void signDeployables(Map<String, Deployable> deployablesMap, Set<Deployable> deployables) {
413
        if (!getDeployer().isSign()) {
1✔
414
            return;
1✔
415
        }
416

417
        verifyKeyIsValid();
1✔
418

419
        for (Deployable deployable : deployablesMap.values()) {
1✔
420
            if (deployable.isSignature() || deployable.isChecksum() || deployable.isMavenMetadata()) continue;
1✔
421

422
            Deployable signedDeployable = deployable.deriveByFilename(deployable.getFilename() + EXT_ASC);
1✔
423

424
            if (isNewer(signedDeployable, deployable)) {
1✔
425
                continue;
1✔
426
            }
427

428
            try {
429
                context.getLogger().setPrefix("sign");
1✔
430
                SigningUtils.sign(context.asImmutable(), deployable.getLocalPath());
1✔
431
                deployables.add(signedDeployable);
1✔
432
            } catch (SigningException e) {
×
433
                throw new JReleaserException(RB.$("ERROR_unexpected_error_signing_file", deployable.getFilename()), e);
×
434
            } finally {
435
                context.getLogger().restorePrefix();
1✔
436
            }
437
        }
1✔
438
    }
1✔
439

440
    private boolean isNewer(Deployable source, Deployable target) {
441
        Path sourcePath = source.getLocalPath();
1✔
442
        if (!Files.exists(sourcePath)) {
1✔
443
            return false;
1✔
444
        }
445

446
        Path targetPath = target.getLocalPath();
1✔
447

448
        FileTime sourceLastModifiedTime = null;
1✔
449
        FileTime targetLastModifiedTime = null;
1✔
450

451
        try {
452
            sourceLastModifiedTime = Files.getLastModifiedTime(sourcePath);
1✔
453
        } catch (IOException e) {
×
454
            throw new JReleaserException(RB.$("ERROR_unexpected_error_timestamp_file", sourcePath.getFileName()), e);
×
455
        }
1✔
456

457
        try {
458
            targetLastModifiedTime = Files.getLastModifiedTime(targetPath);
1✔
459
        } catch (IOException e) {
×
460
            throw new JReleaserException(RB.$("ERROR_unexpected_error_timestamp_file", targetPath.getFileName()), e);
×
461
        }
1✔
462

463
        return sourceLastModifiedTime.compareTo(targetLastModifiedTime) > 0;
1✔
464
    }
465

466
    private void verifyKeyIsValid() {
467
        if (context.getModel().getSigning().getMode() == Signing.Mode.COMMAND ||
1✔
468
            context.getModel().getSigning().getMode() == Signing.Mode.COSIGN) {
1✔
469
            return;
×
470
        }
471

472
        Optional<String> publicKeyID;
473
        Optional<String> fingerprint;
474

475
        boolean verify = context.getModel().getSigning().isVerify();
1✔
476

477
        try {
478
            publicKeyID = SigningUtils.getPublicKeyID(context.asImmutable());
1✔
479
        } catch (SigningException e) {
×
480
            if (verify) {
×
481
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
482
            }
483
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
484
            return;
×
485
        }
1✔
486

487
        if (!publicKeyID.isPresent()) {
1✔
488
            if (verify) {
×
489
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
490
            }
491
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
492
            return;
×
493
        }
494

495
        try {
496
            fingerprint = SigningUtils.getFingerprint(context.asImmutable());
1✔
497
        } catch (SigningException e) {
×
498
            if (verify) {
×
499
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
500
            }
501
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
502
            return;
×
503
        }
1✔
504

505
        if (!fingerprint.isPresent()) {
1✔
506
            if (verify) {
×
507
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
508
            }
509
            context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
510
            return;
×
511
        }
512

513
        String keyID = publicKeyID.get().toUpperCase(Locale.ENGLISH);
1✔
514
        String fp = fingerprint.get().toUpperCase(Locale.ENGLISH);
1✔
515

516
        try {
517
            Optional<Instant> expirationDate = SigningUtils.getExpirationDateOfPublicKey(context.asImmutable());
1✔
518

519
            if (expirationDate.isPresent()) {
1✔
520
                Instant ed = expirationDate.get();
1✔
521
                if (Instant.EPOCH.equals(ed)) {
1✔
522
                    context.getLogger().warn(RB.$("signing.public.key.no.expiration.date", keyID));
×
523

524
                } else if (Instant.now().isAfter(ed)) {
1✔
525
                    LocalDateTime ldt = LocalDateTime.ofInstant(ed, ZoneId.systemDefault());
×
526
                    if (verify) {
×
527
                        throw new JReleaserException(RB.$("ERROR_public_key_expired", keyID, ldt));
×
528
                    } else {
529
                        context.getLogger().warn(RB.$("ERROR_public_key_expired", keyID, ldt));
×
530
                    }
531
                } else {
×
532
                    context.getLogger().info(RB.$("signing.public.key.expiration.date", keyID, LocalDateTime.ofInstant(ed, ZoneId.systemDefault())));
1✔
533
                }
534
            }
535
        } catch (SigningException e) {
×
536
            if (verify) {
×
537
                throw new JReleaserException(RB.$("ERROR_public_key_not_found"));
×
538
            } else {
539
                context.getLogger().warn(RB.$("ERROR_public_key_not_found"));
×
540
                return;
×
541
            }
542
        }
1✔
543

544
        boolean published = false;
1✔
545

546
        context.getLogger().info(RB.$("signing.check.published.key", keyID));
1✔
547
        for (Map.Entry<String, String> e : KEY_SERVERS.entrySet()) {
1✔
548
            try {
549
                boolean notfound = false;
1✔
550
                // 1st try with keyId
551
                URL url = new URI(String.format(e.getValue(), fp)).toURL();
1✔
552
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
1✔
553
                connection.setConnectTimeout(20_000);
1✔
554
                connection.setReadTimeout(40_000);
1✔
555
                if (connection.getResponseCode() < 400) {
1✔
556
                    context.getLogger().debug(" + " + e.getKey());
1✔
557
                    published = true;
1✔
558
                } else {
559
                    notfound = true;
1✔
560
                }
561

562
                if (!notfound) {
1✔
563
                    // 2nd try with fingerprint
564
                    url = new URI(String.format(e.getValue(), keyID)).toURL();
1✔
565
                    connection = (HttpURLConnection) url.openConnection();
1✔
566
                    connection.setConnectTimeout(20_000);
1✔
567
                    connection.setReadTimeout(40_000);
1✔
568
                    if (connection.getResponseCode() < 400) {
1✔
569
                        context.getLogger().debug(" + " + e.getKey());
1✔
570
                        published = true;
1✔
571
                        notfound = false;
1✔
572
                    }
573
                }
574

575
                if (!notfound) {
1✔
576
                    context.getLogger().debug(" x " + e.getKey());
1✔
577
                }
578
            } catch (MalformedURLException | URISyntaxException ignored) {
×
579
                // ignored
580
            } catch (IOException ex) {
×
581
                context.getLogger().debug(RB.$("ERROR_unexpected_error") + " " + ex.getMessage());
×
582
            }
1✔
583
        }
1✔
584

585
        if (published) {
1✔
586
            context.getLogger().info(RB.$("signing.key.published", keyID));
1✔
587
        } else {
588
            context.getLogger().warn(RB.$("signing.key.not.published", keyID));
×
589
        }
590
    }
1✔
591

592
    private void checksumDeployables(Map<String, Deployable> deployablesMap, Set<Deployable> deployables) {
593
        if (!getDeployer().isChecksums()) {
1✔
594
            return;
1✔
595
        }
596

597
        for (Deployable deployable : deployablesMap.values()) {
1✔
598
            if (deployable.isChecksum()) continue;
1✔
599

600
            if (deployable.getFilename().endsWith(EXT_ASC)) {
1✔
601
                // remove checksum for signature files
602
                for (Algorithm algorithm : ALGORITHMS) {
1✔
603
                    Deployable checksumDeployable = deployable.deriveByFilename(deployable.getFilename() + "." + algorithm.formatted());
1✔
604
                    deployables.remove(checksumDeployable);
1✔
605
                }
606
                continue;
1✔
607
            }
608

609
            try {
610
                byte[] data = Files.readAllBytes(deployable.getLocalPath());
1✔
611
                for (Algorithm algorithm : ALGORITHMS) {
1✔
612
                    Deployable checksumDeployable = deployable.deriveByFilename(deployable.getFilename() + "." + algorithm.formatted());
1✔
613

614
                    if (isNewer(checksumDeployable, deployable)) {
1✔
615
                        continue;
1✔
616
                    }
617

618
                    context.getLogger().debug(RB.$("checksum.calculating", algorithm.formatted(), deployable.getFilename()));
1✔
619
                    String checksum = ChecksumUtils.checksum(algorithm, data);
1✔
620
                    Files.write(checksumDeployable.getLocalPath(), checksum.getBytes(UTF_8));
1✔
621
                    deployables.add(checksumDeployable);
1✔
622
                }
623
            } catch (IOException e) {
×
624
                throw new JReleaserException(RB.$("ERROR_unexpected_error_calculate_checksum", deployable.getFilename()), e);
×
625
            }
1✔
626
        }
1✔
627
    }
1✔
628

629
    protected void deployPackages() throws DeployException {
630
        Set<Deployable> deployables = collectDeployables();
1✔
631
        if (deployables.isEmpty()) {
1✔
632
            context.getLogger().info(RB.$("artifacts.no.match"));
×
633
        }
634

635
        D deployer = getDeployer();
1✔
636
        String baseUrl = deployer.getResolvedUrl(context.fullProps());
1✔
637
        String token = deployer.getPassword();
1✔
638

639
        if (!baseUrl.endsWith("/")) {
1✔
640
            baseUrl += " ";
×
641
        }
642

643
        // delete existing packages (if any)
644
        deleteExistingPackages(baseUrl, token, deployables);
1✔
645

646
        for (Deployable deployable : deployables) {
1✔
647
            if (deployable.isSignature() || deployable.isChecksum()) continue;
1✔
648
            Path localPath = deployable.getLocalPath();
1✔
649
            context.getLogger().info(" - {}", deployable.getFilename());
1✔
650

651
            if (!context.isDryrun()) {
1✔
652
                try {
653
                    Map<String, String> headers = new LinkedHashMap<>();
×
654
                    headers.put("Authorization", "Bearer " + token);
×
655
                    FormData data = ClientUtils.toFormData(localPath);
×
656

657
                    String url = baseUrl + deployable.getFullDeployPath();
×
658
                    ClientUtils.putFile(context.getLogger(),
×
659
                        url,
660
                        deployer.getConnectTimeout(),
×
661
                        deployer.getReadTimeout(),
×
662
                        data,
663
                        headers);
664
                } catch (IOException | UploadException e) {
×
665
                    context.getLogger().trace(e);
×
666
                    throw new DeployException(RB.$("ERROR_unexpected_deploy",
×
667
                        context.getBasedir().relativize(localPath), e.getMessage()), e);
×
668
                }
×
669
            }
670
        }
1✔
671
    }
1✔
672

673
    protected void deleteExistingPackages(String baseUrl, String token, Set<Deployable> deployables) throws DeployException {
674
        // noop
675
    }
×
676

677
    private class DeployableCollector extends SimpleFileVisitor<Path> {
678
        private final Path root;
679
        private final Set<Deployable> deployables = new TreeSet<>();
1✔
680
        private final boolean projectIsSnapshot;
681
        private boolean failed;
682

683
        public DeployableCollector(Path root, boolean projectIsSnapshot) {
1✔
684
            this.root = root;
1✔
685
            this.projectIsSnapshot = projectIsSnapshot;
1✔
686
        }
1✔
687

688
        private void match(Path path) {
689
            if (FileUtils.isHidden(path)) return;
1✔
690

691
            String filename = path.getFileName().toString();
1✔
692

693
            if (filename.contains(MAVEN_METADATA_XML)) {
1✔
694
                if (projectIsSnapshot) {
1✔
695
                    addDeployable(path);
×
696
                }
697
            } else {
698
                addDeployable(path);
1✔
699
            }
700
        }
1✔
701

702
        private void addDeployable(Path path) {
703
            String stagingRepository = root.toAbsolutePath().toString();
1✔
704
            String stagingPath = path.getParent().toAbsolutePath().toString();
1✔
705
            PomResult pomResult = parsePom(path);
1✔
706
            deployables.add(new Deployable(
1✔
707
                projectIsSnapshot,
708
                stagingRepository,
709
                stagingPath.substring(stagingRepository.length()),
1✔
710
                pomResult.packaging,
1✔
711
                path.getFileName().toString(),
1✔
712
                pomResult.relocated
1✔
713
            ));
714
        }
1✔
715

716
        private PomResult parsePom(Path artifactPath) {
717
            // only inspect if artifactPath ends with .pom
718
            if (artifactPath.getFileName().toString().endsWith(EXT_JAR)) return new PomResult(PACKAGING_JAR);
1✔
719
            if (!artifactPath.getFileName().toString().endsWith(EXT_POM)) return new PomResult("");
1✔
720

721
            try {
722
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
1✔
723
                factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
1✔
724
                Document document = factory
1✔
725
                    .newDocumentBuilder()
1✔
726
                    .parse(artifactPath.toFile());
1✔
727
                String query = "/project/packaging";
1✔
728
                String packaging = (String) XPathFactory.newInstance()
1✔
729
                    .newXPath()
1✔
730
                    .compile(query)
1✔
731
                    .evaluate(document, XPathConstants.STRING);
1✔
732

733
                query = "/project/distributionManagement/relocation";
1✔
734
                Object relocation = XPathFactory.newInstance()
1✔
735
                    .newXPath()
1✔
736
                    .compile(query)
1✔
737
                    .evaluate(document, XPathConstants.NODE);
1✔
738

739
                return new PomResult(isNotBlank(packaging) ? packaging.trim() : PACKAGING_JAR,
1✔
740
                    relocation != null);
741
            } catch (ParserConfigurationException | IOException | SAXException | XPathExpressionException e) {
×
742
                throw new IllegalStateException(e);
×
743
            }
744
        }
745

746
        @Override
747
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
748
            match(file);
1✔
749
            return CONTINUE;
1✔
750
        }
751

752
        @Override
753
        public FileVisitResult visitFileFailed(Path file, IOException e) throws IOException {
754
            failed = true;
×
755
            context.getLogger().trace(e);
×
756
            context.getLogger().error(RB.$("ERROR_artifacts_unexpected_error_path"),
×
757
                root.toAbsolutePath().relativize(file.toAbsolutePath()), e);
×
758
            return CONTINUE;
×
759
        }
760

761
        private final class PomResult {
762
            private String packaging;
763
            private boolean relocated;
764

765
            private PomResult(String packaging) {
1✔
766
                this.packaging = packaging;
1✔
767
            }
1✔
768

769
            private PomResult(String packaging, boolean relocated) {
1✔
770
                this.packaging = packaging;
1✔
771
                this.relocated = relocated;
1✔
772
            }
1✔
773
        }
774
    }
775
}
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