001/* 002 * JDrupes Builder 003 * Copyright (C) 2026 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.builder.mvnrepo; 020 021import com.google.common.flogger.FluentLogger; 022import java.net.URI; 023import java.util.Collections; 024import java.util.List; 025import java.util.concurrent.atomic.AtomicBoolean; 026import java.util.concurrent.atomic.AtomicInteger; 027import org.eclipse.aether.AbstractRepositoryListener; 028import org.eclipse.aether.DefaultRepositorySystemSession; 029import org.eclipse.aether.RepositoryEvent; 030import org.eclipse.aether.artifact.Artifact; 031import org.eclipse.aether.deployment.DeployRequest; 032import org.eclipse.aether.deployment.DeploymentException; 033import org.eclipse.aether.repository.RemoteRepository; 034import org.eclipse.aether.spi.connector.ArtifactDownload; 035import org.eclipse.aether.transfer.ArtifactNotFoundException; 036import org.eclipse.aether.transfer.NoRepositoryConnectorException; 037import org.eclipse.aether.util.repository.AuthenticationBuilder; 038import org.jdrupes.builder.api.BuildContext; 039import org.jdrupes.builder.api.BuildException; 040 041/// A Maven publishing destination that uploads artifacts using the traditional 042/// Maven deployment approach. 043/// 044/// This implementation of a [MvnPublishingDestination] uploads artifacts 045/// and their associated metadata (like `maven-metadata.xml`) individually 046/// to the specified repository URI. It is typically used for publishing 047/// snapshots or deploying to internal Maven repositories. 048/// 049public class MvnDeployDestination extends MvnPublishingDestination { 050 051 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 052 private String id; 053 private URI repositoryUri; 054 055 /// Initializes a new Maven deploy destination. 056 /// 057 /// @param publicationTypes the supported publication types 058 /// 059 public MvnDeployDestination(MvnVersionType... publicationTypes) { 060 super(publicationTypes); 061 if (publicationTypes.length == 1 062 && publicationTypes[0] == MvnVersionType.SNAPSHOT) { 063 repositoryUri = URI.create( 064 "https://central.sonatype.com/repository/maven-snapshots/"); 065 } 066 } 067 068 /// Sets the Maven repository URI. 069 /// 070 /// @param uri the repository URI 071 /// @return this destination 072 /// 073 public MvnDeployDestination repositoryUri(URI uri) { 074 this.repositoryUri = uri; 075 return this; 076 } 077 078 /// Returns the repository URI. Defaults to 079 /// `https://central.sonatype.com/repository/maven-snapshots/` if 080 /// this destination was created specifically for publication type 081 /// `SNAPSHOT`. 082 /// 083 /// @return the uri 084 /// 085 public URI repositoryUri() { 086 return repositoryUri; 087 } 088 089 @Override 090 /* default */void publish(BuildContext context, MvnPublisher publisher, 091 Artifact mainArtifact, List<Artifact> toDeploy) { 092 // Now deploy everything 093 var session = new DefaultRepositorySystemSession( 094 MavenContext.repositorySession()); 095 session.setRepositoryListener(new UploadListener( 096 context, mainArtifact.getGroupId() + ":" 097 + mainArtifact.getArtifactId() 098 + ":" + mainArtifact.getVersion(), 099 toDeploy.size())); 100 var user = repositoryUser(context); 101 var password = repositoryPassword(context); 102 var repo = new RemoteRepository.Builder("deploy", "default", 103 repositoryUri.toString()) 104 .setAuthentication(new AuthenticationBuilder() 105 .addUsername(user).addPassword(password).build()) 106 .build(); 107 var deployReq = new DeployRequest().setRepository(repo); 108 toDeploy.stream().forEach(deployReq::addArtifact); 109 try { 110 MavenContext.repositorySystem().deploy(session, deployReq); 111 } catch (DeploymentException e) { 112 throw new BuildException().from(publisher).cause(e); 113 } 114 } 115 116 @Override 117 /* default */ boolean alreadyPublished(BuildContext context, 118 Artifact mainArtifact) { 119 var user = repositoryUser(context); 120 var password = repositoryPassword(context); 121 var repo = new RemoteRepository.Builder( 122 id() != null ? ("check-" + id()) : "check", "default", 123 repositoryUri().toString()) 124 .setAuthentication(new AuthenticationBuilder() 125 .addUsername(user).addPassword(password).build()) 126 .build(); 127 try (var connector = MavenContext.repositoryConnectorProvider() 128 .newRepositoryConnector(MavenContext.repositorySession(), repo)) { 129 ArtifactDownload download = new ArtifactDownload(); 130 download.setArtifact(mainArtifact); 131 download.setExistenceCheck(true); 132 connector.get( 133 Collections.singletonList(download), 134 Collections.emptyList()); 135 if (download.getException() == null) { 136 logger.atFinest().log("Artifact %s exists in %s", mainArtifact, 137 repositoryUri); 138 return true; 139 } 140 if (download.getException() instanceof ArtifactNotFoundException) { 141 return false; 142 } 143 throw new BuildException().cause(download.getException()).message( 144 "Cannot check existance of %s on %s: %s", mainArtifact, 145 repositoryUri, download.getException().getMessage()); 146 } catch (NoRepositoryConnectorException e) { 147 throw new BuildException().cause(e).message( 148 "Cannot create connector for %s: %s", repositoryUri, 149 e.getMessage()); 150 } 151 } 152 153 @SuppressWarnings("PMD.CommentRequired") 154 private final class UploadListener extends AbstractRepositoryListener { 155 private final AtomicBoolean startMsgLogged = new AtomicBoolean(false); 156 private final AtomicInteger deployedCount = new AtomicInteger(0); 157 private final String artifact; 158 private final BuildContext context; 159 private final int artifacts; 160 161 @SuppressWarnings("PMD.PublicMemberInNonPublicType") 162 public UploadListener(BuildContext context, String artifact, 163 int artifacts) { 164 this.context = context; 165 this.artifact = artifact; 166 this.artifacts = artifacts; 167 } 168 169 @Override 170 public void artifactDeploying(RepositoryEvent event) { 171 if (!startMsgLogged.getAndSet(true)) { 172 logger.atInfo().log("Start deploying artifacts..."); 173 context.statusLine().update( 174 "%s deploys to %s", this, repositoryUri); 175 } 176 } 177 178 @Override 179 public void artifactDeployed(RepositoryEvent event) { 180 if (!"jar".equals(event.getArtifact().getExtension())) { 181 return; 182 } 183 logger.atInfo().log("Deployed: %s", event.getArtifact()); 184 context.statusLine().update("%s deployed %d/%d", this, 185 deployedCount.incrementAndGet(), artifacts); 186 } 187 188 @Override 189 public void metadataDeployed(RepositoryEvent event) { 190 logger.atInfo().log("Deployed: %s", event.getMetadata()); 191 context.statusLine().update("%s deployed %d/%d", this, 192 deployedCount.incrementAndGet(), artifacts); 193 } 194 195 @Override 196 public String toString() { 197 return "Maven deployer for " + artifact; 198 } 199 200 } 201 202 @Override 203 public String toString() { 204 return "Maven deploy destination " + (id != null ? (id + "::") 205 : "") + repositoryUri; 206 } 207}