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