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}