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.List;
024import java.util.concurrent.atomic.AtomicBoolean;
025import java.util.concurrent.atomic.AtomicInteger;
026import org.eclipse.aether.AbstractRepositoryListener;
027import org.eclipse.aether.DefaultRepositorySystemSession;
028import org.eclipse.aether.RepositoryEvent;
029import org.eclipse.aether.artifact.Artifact;
030import org.eclipse.aether.deployment.DeployRequest;
031import org.eclipse.aether.deployment.DeploymentException;
032import org.eclipse.aether.repository.RemoteRepository;
033import org.eclipse.aether.util.repository.AuthenticationBuilder;
034import org.jdrupes.builder.api.BuildContext;
035import org.jdrupes.builder.api.BuildException;
036
037/// A Maven publishing destination that uploads artifacts using the traditional
038/// Maven deployment approach.
039///
040/// This implementation of a [MvnPublishingDestination] uploads artifacts
041/// and their associated metadata (like `maven-metadata.xml`) individually
042/// to the specified repository URI. It is typically used for publishing
043/// snapshots or deploying to internal Maven repositories.
044///
045public class MvnDeployDestination extends MvnPublishingDestination {
046
047    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
048    private String id;
049    private URI repositoryUri;
050
051    /// Initializes a new Maven deploy destination.
052    ///
053    /// @param publicationTypes the supported publication types
054    ///
055    public MvnDeployDestination(MvnVersionType... publicationTypes) {
056        super(publicationTypes);
057        if (publicationTypes.length == 1
058            && publicationTypes[0] == MvnVersionType.SNAPSHOT) {
059            repositoryUri = URI.create(
060                "https://central.sonatype.com/repository/maven-snapshots/");
061        }
062    }
063
064    /// Sets the Maven repository URI.
065    ///
066    /// @param uri the repository URI
067    /// @return this destination
068    ///
069    public MvnDeployDestination repositoryUri(URI uri) {
070        this.repositoryUri = uri;
071        return this;
072    }
073
074    /// Returns the repository URI. Defaults to
075    /// `https://central.sonatype.com/repository/maven-snapshots/` if
076    /// this destination was created specifically for publication type
077    /// `SNAPSHOT`.
078    ///
079    /// @return the uri
080    ///
081    public URI repositoryUri() {
082        return repositoryUri;
083    }
084
085    @Override
086    /* default */void publish(BuildContext context, MvnPublisher publisher,
087            Artifact mainArtifact, List<Artifact> toDeploy) {
088        // Now deploy everything
089        var session = new DefaultRepositorySystemSession(
090            MavenContext.repositorySession());
091        session.setRepositoryListener(new UploadListener(
092            context, mainArtifact.getGroupId() + ":"
093                + mainArtifact.getArtifactId()
094                + ":" + mainArtifact.getVersion(),
095            toDeploy.size()));
096        var user = repositoryUser(context);
097        var password = repositoryPassword(context);
098        var repo = new RemoteRepository.Builder("deploy", "default",
099            repositoryUri.toString())
100                .setAuthentication(new AuthenticationBuilder()
101                    .addUsername(user).addPassword(password).build())
102                .build();
103        var deployReq = new DeployRequest().setRepository(repo);
104        toDeploy.stream().forEach(deployReq::addArtifact);
105        try {
106            MavenContext.repositorySystem().deploy(session, deployReq);
107        } catch (DeploymentException e) {
108            throw new BuildException().from(publisher).cause(e);
109        }
110    }
111
112    @SuppressWarnings("PMD.CommentRequired")
113    private final class UploadListener extends AbstractRepositoryListener {
114        private final AtomicBoolean startMsgLogged = new AtomicBoolean(false);
115        private final AtomicInteger deployedCount = new AtomicInteger(0);
116        private final String artifact;
117        private final BuildContext context;
118        private final int artifacts;
119
120        @SuppressWarnings("PMD.PublicMemberInNonPublicType")
121        public UploadListener(BuildContext context, String artifact,
122                int artifacts) {
123            this.context = context;
124            this.artifact = artifact;
125            this.artifacts = artifacts;
126        }
127
128        @Override
129        public void artifactDeploying(RepositoryEvent event) {
130            if (!startMsgLogged.getAndSet(true)) {
131                logger.atInfo().log("Start deploying artifacts...");
132                context.statusLine().update(
133                    "%s deploys to %s", this, repositoryUri);
134            }
135        }
136
137        @Override
138        public void artifactDeployed(RepositoryEvent event) {
139            if (!"jar".equals(event.getArtifact().getExtension())) {
140                return;
141            }
142            logger.atInfo().log("Deployed: %s", event.getArtifact());
143            context.statusLine().update("%s deployed %d/%d", this,
144                deployedCount.incrementAndGet(), artifacts);
145        }
146
147        @Override
148        public void metadataDeployed(RepositoryEvent event) {
149            logger.atInfo().log("Deployed: %s", event.getMetadata());
150            context.statusLine().update("%s deployed %d/%d", this,
151                deployedCount.incrementAndGet(), artifacts);
152        }
153
154        @Override
155        public String toString() {
156            return "Maven deployer for " + artifact;
157        }
158
159    }
160
161    @Override
162    public String toString() {
163        return "Maven deploy destination " + (id != null ? (id + "::")
164            : "") + repositoryUri;
165    }
166}