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.net.URI;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.stream.Stream;
032import org.eclipse.aether.RepositorySystem;
033import org.eclipse.aether.RepositorySystemSession;
034import org.eclipse.aether.artifact.Artifact;
035import org.eclipse.aether.artifact.DefaultArtifact;
036import org.eclipse.aether.collection.CollectRequest;
037import org.eclipse.aether.graph.Dependency;
038import org.eclipse.aether.graph.DependencyNode;
039import org.eclipse.aether.repository.RemoteRepository;
040import org.eclipse.aether.repository.RepositoryPolicy;
041import org.eclipse.aether.resolution.ArtifactRequest;
042import org.eclipse.aether.resolution.ArtifactResolutionException;
043import org.eclipse.aether.resolution.DependencyRequest;
044import org.eclipse.aether.resolution.DependencyResolutionException;
045import org.eclipse.aether.util.artifact.SubArtifact;
046import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
047import org.jdrupes.builder.api.BuildException;
048import org.jdrupes.builder.api.Resource;
049import org.jdrupes.builder.api.ResourceFactory;
050import org.jdrupes.builder.api.ResourceProvider;
051import org.jdrupes.builder.api.ResourceRequest;
052import org.jdrupes.builder.api.ResourceType;
053import static org.jdrupes.builder.api.ResourceType.*;
054import org.jdrupes.builder.api.Resources;
055import org.jdrupes.builder.core.AbstractProvider;
056import org.jdrupes.builder.java.CompilationResources;
057import org.jdrupes.builder.java.LibraryJarFile;
058import org.jdrupes.builder.mvnrepo.MvnRepoDependency.Scope;
059import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*;
060
061/// Depending on the request, this provider provides two types of resources.
062/// 
063///  1. The artifacts to be resolved as
064///     `CompilationResources<MavenRepoDependencies>`. The artifacts
065///     to be resolved are those added with [resolve].
066///
067///  2. The `CompilationResources<LibraryJarFile>` 
068///     or `RuntimeResources<LibraryJarFile>` (depending on the
069///     request) that result from resolving the artifacts to be resolved.
070///     The resources returned implement the additional marker interface
071///     `MvnRepoJarFile`.
072///
073public class MvnRepoLookup extends AbstractProvider
074        implements ResourceProvider {
075
076    private final Map<ResourceType<? extends Resources<?>>,
077            List<String>> coordinates = new ConcurrentHashMap<>();
078    private boolean downloadSources = true;
079    private boolean downloadJavadoc = true;
080    private URI snapshotUri;
081    private static Context rootContextInstance;
082
083    /// Instantiates a new mvn repo lookup.
084    ///
085    @SuppressWarnings("PMD.UnnecessaryConstructor")
086    public MvnRepoLookup() {
087        // Make javadoc happy.
088    }
089
090    /// Lazily creates the root context.
091    /// @return the context
092    ///
093    /* default */ static Context rootContext() {
094        if (rootContextInstance != null) {
095            return rootContextInstance;
096        }
097        ContextOverrides overrides = ContextOverrides.create()
098            .withUserSettings(true).build();
099        Runtime runtime = Runtimes.INSTANCE.getRuntime();
100        rootContextInstance = runtime.create(overrides);
101        return rootContextInstance;
102    }
103
104    /// Sets the Maven snapshot repository URI.
105    ///
106    /// @param uri the snapshot repository URI
107    /// @return the mvn repo lookup
108    ///
109    public MvnRepoLookup snapshotRepository(URI uri) {
110        this.snapshotUri = uri;
111        return this;
112    }
113
114    /// Returns the snapshot repository. Defaults to
115    /// `https://central.sonatype.com/repository/maven-snapshots/`.
116    ///
117    /// @return the uri
118    ///
119    public URI snapshotRepository() {
120        return snapshotUri;
121    }
122
123    /// Add artifacts, specified by their coordinates
124    /// (`groupId:artifactId:version`) with the given scope.
125    ///
126    /// @param scope the scope
127    /// @param coordinates the coordinates
128    /// @return the mvn repo lookup
129    ///
130    public MvnRepoLookup resolve(Scope scope, String... coordinates) {
131        this.coordinates
132            .computeIfAbsent(scope == Scope.Compile ? MvnRepoCompilationDepsType
133                : MvnRepoRuntimeDepsType, _ -> new ArrayList<>())
134            .addAll(Arrays.asList(coordinates));
135        return this;
136    }
137
138    /// Add artifacts, specified by their coordinates
139    /// (`groupId:artifactId:version`) as compilation resources.
140    ///
141    /// @param coordinates the coordinates
142    /// @return the mvn repo lookup
143    ///
144    public MvnRepoLookup resolve(String... coordinates) {
145        return resolve(Scope.Compile, coordinates);
146    }
147
148    /// Whether to also download the sources. Defaults to `true`.
149    ///
150    /// @param enable the enable
151    /// @return the mvn repo lookup
152    ///
153    public MvnRepoLookup downloadSources(boolean enable) {
154        this.downloadSources = enable;
155        return this;
156    }
157
158    /// Whether to also download the javadoc. Defaults to `true`.
159    ///
160    /// @param enable the enable
161    /// @return the mvn repo lookup
162    ///
163    public MvnRepoLookup downloadJavadoc(boolean enable) {
164        this.downloadJavadoc = enable;
165        return this;
166    }
167
168    /// Provide.
169    ///
170    /// @param <T> the generic type
171    /// @param requested the requested resources
172    /// @return the stream
173    ///
174    @Override
175    protected <T extends Resource> Stream<T>
176            doProvide(ResourceRequest<T> requested) {
177        if (requested.accepts(MvnRepoCompilationDepsType)) {
178            @SuppressWarnings("unchecked")
179            var result = (Stream<T>) coordinates.entrySet().stream()
180                .filter(e -> requested.accepts(e.getKey()))
181                .map(e -> e.getValue().stream().map(c -> ResourceFactory
182                    .create(MvnRepoDependencyType, null, c,
183                        e.getKey().equals(MvnRepoCompilationDepsType)
184                            ? Scope.Compile
185                            : Scope.Runtime)))
186                .flatMap(d -> d);
187            return result;
188        }
189        if (requested.accepts(
190            new ResourceType<CompilationResources<LibraryJarFile>>() {})) {
191            return provideJars(requested);
192        }
193        return Stream.empty();
194    }
195
196    private <T extends Resource> Stream<T>
197            provideJars(ResourceRequest<T> requested) {
198        // Base collect request, optionally with snapshots
199        CollectRequest collectRequest = new CollectRequest().setRepositories(
200            new ArrayList<>(rootContext().remoteRepositories()));
201        if (snapshotUri != null) {
202            addSnapshotRepository(collectRequest);
203        }
204
205        // Retrieve coordinates and add to collect request
206        var asDepsType = resourceType(
207            requested.type().rawType(), MvnRepoDependency.class);
208        coordinates.entrySet().stream()
209            .filter(e -> asDepsType.isAssignableFrom(e.getKey()))
210            .forEach(e -> e.getValue().stream().forEach(c -> collectRequest
211                .addDependency(new Dependency(new DefaultArtifact(c),
212                    e.getKey().equals(MvnRepoCompilationDepsType) ? "compile"
213                        : "runtime"))));
214
215        DependencyRequest dependencyRequest
216            = new DependencyRequest(collectRequest, null);
217        DependencyNode rootNode;
218        try {
219            var repoSystem = rootContext().repositorySystem();
220            var repoSession = rootContext().repositorySystemSession();
221            rootNode = repoSystem.resolveDependencies(repoSession,
222                dependencyRequest).getRoot();
223// For maven 2.x libraries:
224//                List<DependencyNode> dependencyNodes = new ArrayList<>();
225//                rootNode.accept(new PreorderDependencyNodeConsumerVisitor(
226//                    dependencyNodes::add));
227            PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
228            rootNode.accept(nlg);
229            List<DependencyNode> dependencyNodes = nlg.getNodes();
230            @SuppressWarnings("unchecked")
231            var result = (Stream<T>) dependencyNodes.stream()
232                .filter(d -> d.getArtifact() != null)
233                .map(DependencyNode::getArtifact)
234                .map(a -> {
235                    if (downloadSources) {
236                        downloadSourceJar(repoSystem, repoSession, a);
237                    }
238                    if (downloadJavadoc) {
239                        downloadJavadocJar(repoSystem, repoSession, a);
240                    }
241                    return a;
242                }).map(a -> a.getFile().toPath())
243                .map(p -> ResourceFactory.create(MvnRepoLibraryJarFileType, p));
244            return result;
245        } catch (DependencyResolutionException e) {
246            throw new BuildException(
247                "Cannot resolve: " + e.getMessage(), e);
248        }
249    }
250
251    private void addSnapshotRepository(CollectRequest collectRequest) {
252        RemoteRepository snapshotsRepo = new RemoteRepository.Builder(
253            "snapshots", "default", snapshotUri.toString())
254                .setSnapshotPolicy(new RepositoryPolicy(
255                    true,  // enable snapshots
256                    RepositoryPolicy.UPDATE_POLICY_ALWAYS,
257                    RepositoryPolicy.CHECKSUM_POLICY_WARN))
258                .setReleasePolicy(new RepositoryPolicy(
259                    false,
260                    RepositoryPolicy.UPDATE_POLICY_NEVER,
261                    RepositoryPolicy.CHECKSUM_POLICY_IGNORE))
262                .build();
263        collectRequest.addRepository(snapshotsRepo);
264    }
265
266    private void downloadSourceJar(RepositorySystem repoSystem,
267            RepositorySystemSession repoSession, Artifact jarArtifact) {
268        Artifact sourcesArtifact
269            = new SubArtifact(jarArtifact, "sources", "jar");
270        ArtifactRequest sourcesRequest = new ArtifactRequest();
271        sourcesRequest.setArtifact(sourcesArtifact);
272        sourcesRequest.setRepositories(rootContext().remoteRepositories());
273        try {
274            repoSystem.resolveArtifact(repoSession, sourcesRequest);
275        } catch (ArtifactResolutionException e) { // NOPMD
276            // Ignore, sources are optional
277        }
278    }
279
280    private void downloadJavadocJar(RepositorySystem repoSystem,
281            RepositorySystemSession repoSession, Artifact jarArtifact) {
282        Artifact javadocArtifact
283            = new SubArtifact(jarArtifact, "javadoc", "jar");
284        ArtifactRequest sourcesRequest = new ArtifactRequest();
285        sourcesRequest.setArtifact(javadocArtifact);
286        sourcesRequest.setRepositories(rootContext().remoteRepositories());
287        try {
288            repoSystem.resolveArtifact(repoSession, sourcesRequest);
289        } catch (ArtifactResolutionException e) { // NOPMD
290            // Ignore, javadoc is optional
291        }
292    }
293}