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