001/*
002 * JDrupes Builder
003 * Copyright (C) 2025 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 eu.maveniverse.maven.mima.context.Context;
022import eu.maveniverse.maven.mima.context.ContextOverrides;
023import eu.maveniverse.maven.mima.context.Runtime;
024import eu.maveniverse.maven.mima.context.Runtimes;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.List;
028import java.util.stream.Stream;
029import org.eclipse.aether.RepositorySystem;
030import org.eclipse.aether.RepositorySystemSession;
031import org.eclipse.aether.artifact.Artifact;
032import org.eclipse.aether.artifact.DefaultArtifact;
033import org.eclipse.aether.collection.CollectRequest;
034import org.eclipse.aether.graph.Dependency;
035import org.eclipse.aether.graph.DependencyNode;
036import org.eclipse.aether.resolution.ArtifactRequest;
037import org.eclipse.aether.resolution.ArtifactResolutionException;
038import org.eclipse.aether.resolution.DependencyRequest;
039import org.eclipse.aether.resolution.DependencyResolutionException;
040import org.eclipse.aether.util.artifact.SubArtifact;
041import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
042import org.jdrupes.builder.api.BuildException;
043import org.jdrupes.builder.api.Resource;
044import org.jdrupes.builder.api.ResourceFactory;
045import org.jdrupes.builder.api.ResourceProvider;
046import org.jdrupes.builder.api.ResourceRequest;
047import org.jdrupes.builder.core.AbstractProvider;
048import org.jdrupes.builder.java.CompilationResources;
049import static org.jdrupes.builder.java.JavaTypes.*;
050import org.jdrupes.builder.java.RuntimeResources;
051import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*;
052
053/// Depending on the request, this provider provides two types of resources.
054/// 
055///  1. The artifacts to be resolved as [MvnRepoDependencies]. The artifacts
056///     to be resolved are those added with [resolve].
057///
058///  2. The [CompilationResources] or [RuntimeResources] (depending on the
059///     request) that result from resolving the artifacts to be resolved.
060///
061public class MvnRepoLookup extends AbstractProvider
062        implements ResourceProvider {
063
064    private final List<String> coordinates = new ArrayList<>();
065    private boolean downloadSources = true;
066    private boolean downloadJavadoc = true;
067    private static Context rootContextInstance;
068
069    /// Instantiates a new mvn repo lookup.
070    ///
071    @SuppressWarnings("PMD.UnnecessaryConstructor")
072    public MvnRepoLookup() {
073        // Make javadoc happy.
074    }
075
076    /// Lazily creates the root context.
077    /// @return the context
078    ///
079    /* default */ static Context rootContext() {
080        if (rootContextInstance != null) {
081            return rootContextInstance;
082        }
083        ContextOverrides overrides = ContextOverrides.create()
084            .withUserSettings(true).build();
085        Runtime runtime = Runtimes.INSTANCE.getRuntime();
086        rootContextInstance = runtime.create(overrides);
087        return rootContextInstance;
088    }
089
090    /// Add artifacts, specified by their coordinates
091    /// (`groupId:artifactId:version`).
092    ///
093    /// @param coordinates the coordinates
094    /// @return the mvn repo lookup
095    ///
096    public MvnRepoLookup resolve(String... coordinates) {
097        this.coordinates.addAll(Arrays.asList(coordinates));
098        return this;
099    }
100
101    /// Whether to also download the sources. Defaults to `true`.
102    ///
103    /// @param enable the enable
104    /// @return the mvn repo lookup
105    ///
106    public MvnRepoLookup downloadSources(boolean enable) {
107        this.downloadSources = enable;
108        return this;
109    }
110
111    /// Whether to also download the javadoc. Defaults to `true`.
112    ///
113    /// @param enable the enable
114    /// @return the mvn repo lookup
115    ///
116    public MvnRepoLookup downloadJavadoc(boolean enable) {
117        this.downloadJavadoc = enable;
118        return this;
119    }
120
121    /// Provide.
122    ///
123    /// @param <T> the generic type
124    /// @param requested the requested resources
125    /// @return the stream
126    ///
127    @Override
128    protected <T extends Resource> Stream<T>
129            doProvide(ResourceRequest<T> requested) {
130        if (requested.wants(MvnRepoDependenciesType)) {
131            @SuppressWarnings("unchecked")
132            var result = (Stream<T>) coordinates.stream()
133                .map(c -> ResourceFactory.create(MvnRepoDependencyType, null,
134                    c));
135            return result;
136        }
137        if (requested.wants(CompilationResourcesType)
138            && requested.includes(JarFileType)) {
139            return provideJars(requested);
140        }
141        return Stream.empty();
142    }
143
144    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
145    private <T extends Resource> Stream<T>
146            provideJars(ResourceRequest<T> requested) {
147        CollectRequest collectRequest = new CollectRequest()
148            .setRepositories(rootContext().remoteRepositories());
149        for (var coord : coordinates) {
150            collectRequest.addDependency(
151                new Dependency(new DefaultArtifact(coord),
152                    requested.wants(CompilationResourcesType) ? "compile"
153                        : "runtime"));
154        }
155
156        DependencyRequest dependencyRequest
157            = new DependencyRequest(collectRequest, null);
158        DependencyNode rootNode;
159        try {
160            var repoSystem = rootContext().repositorySystem();
161            var repoSession = rootContext().repositorySystemSession();
162            rootNode = repoSystem.resolveDependencies(repoSession,
163                dependencyRequest).getRoot();
164// For maven 2.x libraries:
165//                List<DependencyNode> dependencyNodes = new ArrayList<>();
166//                rootNode.accept(new PreorderDependencyNodeConsumerVisitor(
167//                    dependencyNodes::add));
168            PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
169            rootNode.accept(nlg);
170            List<DependencyNode> dependencyNodes = nlg.getNodes();
171            @SuppressWarnings("unchecked")
172            var result = (Stream<T>) dependencyNodes.stream()
173                .filter(d -> d.getArtifact() != null)
174                .map(DependencyNode::getArtifact)
175                .map(a -> {
176                    if (downloadSources) {
177                        downloadSourceJar(repoSystem, repoSession, a);
178                    }
179                    if (downloadJavadoc) {
180                        downloadJavadocJar(repoSystem, repoSession, a);
181                    }
182                    return a;
183                }).map(a -> a.getFile().toPath())
184                .map(p -> ResourceFactory.create(MvnRepoJarFileType, p));
185            return result;
186        } catch (DependencyResolutionException e) {
187            throw new BuildException(
188                "Cannot resolve: " + e.getMessage(), e);
189        }
190    }
191
192    private void downloadSourceJar(RepositorySystem repoSystem,
193            RepositorySystemSession repoSession, Artifact jarArtifact) {
194        Artifact sourcesArtifact
195            = new SubArtifact(jarArtifact, "sources", "jar");
196        ArtifactRequest sourcesRequest = new ArtifactRequest();
197        sourcesRequest.setArtifact(sourcesArtifact);
198        sourcesRequest.setRepositories(rootContext().remoteRepositories());
199        try {
200            repoSystem.resolveArtifact(repoSession, sourcesRequest);
201        } catch (ArtifactResolutionException e) { // NOPMD
202            // Ignore, sources are optional
203        }
204    }
205
206    private void downloadJavadocJar(RepositorySystem repoSystem,
207            RepositorySystemSession repoSession, Artifact jarArtifact) {
208        Artifact javadocArtifact
209            = new SubArtifact(jarArtifact, "javadoc", "jar");
210        ArtifactRequest sourcesRequest = new ArtifactRequest();
211        sourcesRequest.setArtifact(javadocArtifact);
212        sourcesRequest.setRepositories(rootContext().remoteRepositories());
213        try {
214            repoSystem.resolveArtifact(repoSession, sourcesRequest);
215        } catch (ArtifactResolutionException e) { // NOPMD
216            // Ignore, javadoc is optional
217        }
218    }
219}