001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 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 eu.maveniverse.maven.mima.context.Context;
023import eu.maveniverse.maven.mima.context.ContextOverrides;
024import eu.maveniverse.maven.mima.context.Runtime;
025import eu.maveniverse.maven.mima.context.Runtimes;
026import java.net.URI;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.List;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034import org.apache.maven.model.DependencyManagement;
035import org.apache.maven.model.Model;
036import org.apache.maven.model.building.DefaultModelBuilderFactory;
037import org.apache.maven.model.building.DefaultModelBuildingRequest;
038import org.apache.maven.model.building.ModelBuildingException;
039import org.apache.maven.model.building.ModelBuildingRequest;
040import org.eclipse.aether.RepositorySystem;
041import org.eclipse.aether.RepositorySystemSession;
042import org.eclipse.aether.artifact.Artifact;
043import org.eclipse.aether.collection.CollectRequest;
044import org.eclipse.aether.graph.DependencyNode;
045import org.eclipse.aether.repository.RemoteRepository;
046import org.eclipse.aether.repository.RepositoryPolicy;
047import org.eclipse.aether.resolution.ArtifactRequest;
048import org.eclipse.aether.resolution.ArtifactResolutionException;
049import org.eclipse.aether.resolution.DependencyRequest;
050import org.eclipse.aether.resolution.DependencyResolutionException;
051import org.eclipse.aether.util.artifact.SubArtifact;
052import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
053import org.jdrupes.builder.api.BuildException;
054import org.jdrupes.builder.api.Resource;
055import org.jdrupes.builder.api.ResourceFactory;
056import org.jdrupes.builder.api.ResourceRequest;
057import org.jdrupes.builder.core.AbstractProvider;
058import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*;
059
060/// Depending on the request, this provider provides two types of resources.
061/// 
062///  1. The artifacts to be resolved as resources of type [MvnRepoDependency].
063///     The artifacts to be resolved are those added with [resolve].
064///     Note that the result also includes the [MvnRepoBom]s.
065///
066///  2. The resources of type [MvnRepoLibraryJarFile] that result from
067///     resolving the artifacts to be resolved.
068///
069@SuppressWarnings("PMD.CouplingBetweenObjects")
070public class MvnRepoLookup extends AbstractProvider {
071
072    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
073    private static Context rootContextInstance;
074    private final List<String> coordinates = new ArrayList<>();
075    private final List<String> boms = new ArrayList<>();
076    private boolean downloadSources = true;
077    private boolean downloadJavadoc = true;
078    private URI snapshotUri;
079    private boolean probeMode;
080
081    /// Instantiates a new mvn repo lookup.
082    ///
083    public MvnRepoLookup() {
084        // Make javadoc happy.
085    }
086
087    /// Lazily creates the root context.
088    /// @return the context
089    ///
090    /* default */ static Context rootContext() {
091        if (rootContextInstance != null) {
092            return rootContextInstance;
093        }
094        ContextOverrides overrides = ContextOverrides.create()
095            .withUserSettings(true).build();
096        Runtime runtime = Runtimes.INSTANCE.getRuntime();
097        rootContextInstance = runtime.create(overrides);
098        logger.atFine().log("Using repositories: %s",
099            rootContextInstance.remoteRepositories().stream()
100                .map(RemoteRepository::getUrl)
101                .collect(Collectors.joining(", ")));
102        return rootContextInstance;
103    }
104
105    /// Sets the Maven snapshot repository URI.
106    ///
107    /// @param uri the snapshot repository URI
108    /// @return the mvn repo lookup
109    ///
110    public MvnRepoLookup snapshotRepository(URI uri) {
111        this.snapshotUri = uri;
112        return this;
113    }
114
115    /// Returns the snapshot repository. Defaults to
116    /// `https://central.sonatype.com/repository/maven-snapshots/`.
117    ///
118    /// @return the uri
119    ///
120    public URI snapshotRepository() {
121        return snapshotUri;
122    }
123
124    /// Add a bill of materials. The coordinates are resolved as 
125    /// a dependency with scope `import` which is added to the
126    /// `dependencyManagement` section.
127    ///
128    /// @param coordinates the coordinates
129    /// @return the mvn repo lookup
130    ///
131    public MvnRepoLookup bom(String... coordinates) {
132        boms.addAll(Arrays.asList(coordinates));
133        return this;
134    }
135
136    /// Add artifacts, specified by their coordinates
137    /// (`groupId:artifactId:version`) as resources.
138    ///
139    /// @param coordinates the coordinates
140    /// @return the mvn repo lookup
141    ///
142    public MvnRepoLookup resolve(String... coordinates) {
143        this.coordinates.addAll(Arrays.asList(coordinates));
144        return this;
145    }
146
147    /// Add artifacts. The method handles [MvnRepoBom]s correctly.
148    ///
149    /// @param resources the resources
150    /// @return the mvn repo lookup
151    ///
152    public MvnRepoLookup resolve(Stream<? extends MvnRepoResource> resources) {
153        resources.forEach(r -> {
154            if (r instanceof MvnRepoBom) {
155                bom(r.coordinates());
156            } else {
157                resolve(r.coordinates());
158            }
159        });
160        return this;
161    }
162
163    /// Failing to resolve the dependencies normally results in a
164    /// [BuildException], because the requested artifacts are assumed
165    /// to be required for the built.
166    /// 
167    /// By invoking this method the provider enters probe mode
168    /// and returns an empty result stream instead of throwing an
169    /// exception if the resolution fails.
170    ///
171    /// @return the mvn repo lookup
172    ///
173    public MvnRepoLookup probe() {
174        probeMode = true;
175        logger.atFine().log("Probe mode enabled for %s", this);
176        return this;
177    }
178
179    /// Whether to also download the sources. Defaults to `true`.
180    ///
181    /// @param enable the enable
182    /// @return the mvn repo lookup
183    ///
184    public MvnRepoLookup downloadSources(boolean enable) {
185        this.downloadSources = enable;
186        return this;
187    }
188
189    /// Whether to also download the javadoc. Defaults to `true`.
190    ///
191    /// @param enable the enable
192    /// @return the mvn repo lookup
193    ///
194    public MvnRepoLookup downloadJavadoc(boolean enable) {
195        this.downloadJavadoc = enable;
196        return this;
197    }
198
199    /// Provide.
200    ///
201    /// @param <T> the generic type
202    /// @param request the requested resources
203    /// @return the stream
204    ///
205    @Override
206    protected <T extends Resource> Collection<T>
207            doProvide(ResourceRequest<T> request) {
208        if (request.accepts(MvnRepoDependencyType)) {
209            return provideMvnDeps();
210        }
211        if (!request.accepts(MvnRepoLibraryJarFileType)) {
212            return Collections.emptyList();
213        }
214        if (!request.isFor(MvnRepoLibraryJarFileType)) {
215            @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
216            var result = (Collection<T>) context()
217                .resources(this, of(MvnRepoLibraryJarFileType)).toList();
218            return result;
219        }
220        try {
221            return provideJars();
222        } catch (ModelBuildingException e) {
223            throw new BuildException().from(this).cause(e);
224        } catch (DependencyResolutionException e) {
225            if (probeMode) {
226                return Collections.emptyList();
227            }
228            throw new BuildException().from(this).cause(e);
229        }
230    }
231
232    private <T extends Resource> Collection<T> provideMvnDeps() {
233        @SuppressWarnings("unchecked")
234        var boms = (Stream<T>) this.boms.stream()
235            .map(MvnRepoBom::of);
236        @SuppressWarnings("unchecked")
237        var deps = (Stream<T>) coordinates.stream()
238            .map(MvnRepoDependency::of);
239        return Stream.concat(boms, deps).toList();
240    }
241
242    private <T extends Resource> Collection<T> provideJars()
243            throws DependencyResolutionException, ModelBuildingException {
244        var repoSystem = rootContext().repositorySystem();
245        var repoSession = rootContext().repositorySystemSession();
246        var repos = new ArrayList<>(rootContext().remoteRepositories());
247        if (snapshotUri != null) {
248            repos.add(createSnapshotRepository());
249        }
250
251        // Create an effective model. This make sure that BOMs are
252        // handled correctly.
253        Model model = getEffectiveModel(repoSystem, repoSession,
254            repos);
255
256        // Create collect request using data from the (effective) model
257        CollectRequest collectRequest
258            = new CollectRequest().setRepositories(repos);
259        collectRequest.setManagedDependencies(
260            model.getDependencyManagement().getDependencies().stream()
261                .map(DependencyConverter::convert).toList());
262        model.getDependencies().stream().map(DependencyConverter::convert)
263            .forEach(collectRequest::addDependency);
264
265        // Resolve dependencies
266        DependencyRequest dependencyRequest
267            = new DependencyRequest(collectRequest, null);
268        DependencyNode rootNode;
269        rootNode = repoSystem.resolveDependencies(repoSession,
270            dependencyRequest).getRoot();
271// For maven 2.x libraries:
272//                List<DependencyNode> dependencyNodes = new ArrayList<>();
273//                rootNode.accept(new PreorderDependencyNodeConsumerVisitor(
274//                    dependencyNodes::add));
275        PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
276        rootNode.accept(nlg);
277        List<DependencyNode> dependencyNodes = nlg.getNodes();
278        @SuppressWarnings("unchecked")
279        var result = (Collection<T>) dependencyNodes.stream()
280            .filter(d -> d.getArtifact() != null)
281            .map(DependencyNode::getArtifact)
282            .map(a -> {
283                if (downloadSources) {
284                    downloadSourceJar(repoSystem, repoSession, a);
285                }
286                if (downloadJavadoc) {
287                    downloadJavadocJar(repoSystem, repoSession, a);
288                }
289                return a;
290            }).map(a -> a.getFile().toPath())
291            .map(p -> ResourceFactory.create(MvnRepoLibraryJarFileType, p))
292            .toList();
293        return result;
294    }
295
296    private Model getEffectiveModel(RepositorySystem repoSystem,
297            RepositorySystemSession repoSession, List<RemoteRepository> repos)
298            throws ModelBuildingException {
299        // First build raw model
300        Model model = new Model();
301        model.setModelVersion("4.0.0");
302        model.setGroupId("model.group");
303        model.setArtifactId("model.artifact");
304        model.setVersion("0.0.0");
305        var depMgmt = new DependencyManagement();
306        model.setDependencyManagement(depMgmt);
307        boms.stream().forEach(c -> {
308            var mvnResource = MvnRepoDependency.of(c);
309            var dep = DependencyConverter.convert(mvnResource, "import");
310            dep.setType("pom");
311            depMgmt.addDependency(dep);
312        });
313        coordinates.forEach(c -> {
314            var mvnResource = MvnRepoDependency.of(c);
315            model.addDependency(
316                DependencyConverter.convert(mvnResource, "compile"));
317        });
318
319        // Now build (derive) effective model
320        var buildingRequest = new DefaultModelBuildingRequest()
321            .setRawModel(model).setProcessPlugins(false)
322            .setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
323            .setModelResolver(
324                new MvnModelResolver(repoSystem, repoSession, repos));
325        return new DefaultModelBuilderFactory()
326            .newInstance().build(buildingRequest).getEffectiveModel();
327    }
328
329    private RemoteRepository createSnapshotRepository() {
330        return new RemoteRepository.Builder(
331            "snapshots", "default", snapshotUri.toString())
332                .setSnapshotPolicy(new RepositoryPolicy(
333                    true,  // enable snapshots
334                    RepositoryPolicy.UPDATE_POLICY_ALWAYS,
335                    RepositoryPolicy.CHECKSUM_POLICY_WARN))
336                .setReleasePolicy(new RepositoryPolicy(
337                    false,
338                    RepositoryPolicy.UPDATE_POLICY_NEVER,
339                    RepositoryPolicy.CHECKSUM_POLICY_IGNORE))
340                .build();
341    }
342
343    private void downloadSourceJar(RepositorySystem repoSystem,
344            RepositorySystemSession repoSession, Artifact jarArtifact) {
345        Artifact sourcesArtifact
346            = new SubArtifact(jarArtifact, "sources", "jar");
347        ArtifactRequest sourcesRequest = new ArtifactRequest();
348        sourcesRequest.setArtifact(sourcesArtifact);
349        sourcesRequest.setRepositories(rootContext().remoteRepositories());
350        try {
351            repoSystem.resolveArtifact(repoSession, sourcesRequest);
352        } catch (ArtifactResolutionException e) { // NOPMD
353            // Ignore, sources are optional
354        }
355    }
356
357    private void downloadJavadocJar(RepositorySystem repoSystem,
358            RepositorySystemSession repoSession, Artifact jarArtifact) {
359        Artifact javadocArtifact
360            = new SubArtifact(jarArtifact, "javadoc", "jar");
361        ArtifactRequest sourcesRequest = new ArtifactRequest();
362        sourcesRequest.setArtifact(javadocArtifact);
363        sourcesRequest.setRepositories(rootContext().remoteRepositories());
364        try {
365            repoSystem.resolveArtifact(repoSession, sourcesRequest);
366        } catch (ArtifactResolutionException e) { // NOPMD
367            // Ignore, javadoc is optional
368        }
369    }
370}