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.startup;
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.MalformedURLException;
026import java.net.URL;
027import java.net.URLClassLoader;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.List;
031import java.util.logging.Level;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034import org.apache.commons.cli.CommandLine;
035import org.apache.commons.cli.DefaultParser;
036import org.apache.commons.cli.ParseException;
037import org.eclipse.aether.artifact.DefaultArtifact;
038import org.eclipse.aether.collection.CollectRequest;
039import org.eclipse.aether.graph.Dependency;
040import org.eclipse.aether.graph.DependencyNode;
041import org.eclipse.aether.resolution.DependencyRequest;
042import org.eclipse.aether.resolution.DependencyResolutionException;
043import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
044import org.jdrupes.builder.api.BuildException;
045import org.jdrupes.builder.api.Launcher;
046import org.jdrupes.builder.api.Masked;
047import org.jdrupes.builder.api.Project;
048import org.jdrupes.builder.api.Resource;
049import org.jdrupes.builder.api.ResourceFactory;
050import org.jdrupes.builder.api.ResourceRequest;
051import org.jdrupes.builder.api.RootProject;
052import org.jdrupes.builder.core.LauncherSupport;
053import org.jdrupes.builder.java.JarFile;
054import static org.jdrupes.builder.java.JavaTypes.*;
055
056/// An implementation of a [Launcher] that expects that the JDrupes
057/// Builder project already been compiled and its classes are available
058/// on the classpath.
059///
060public class DirectLauncher extends AbstractLauncher {
061
062    private static final String RUNTIME_EXTENSIONS = "runtimeExtensions";
063    private RootProject rootProject;
064
065    /// Instantiates a new direct launcher. The classpath is scanned for
066    /// classes that implement [Project] but do not implement [Masked].
067    /// One of these must also implement the [RootProject] interface.
068    /// The latter is instantiated and registered as root project with all
069    /// other classes found as direct sub projects.
070    ///
071    /// @param classloader the classloader
072    /// @param args the arguments
073    ///
074    @SuppressWarnings({ "PMD.UseVarargs", "PMD.SystemPrintln" })
075    public DirectLauncher(ClassLoader classloader, String[] args) {
076        super(args);
077        unwrapBuildException(() -> {
078            final var extClsLdr = addRuntimeExts(classloader);
079            var rootProjects = new ArrayList<Class<? extends RootProject>>();
080            var subprojects = new ArrayList<Class<? extends Project>>();
081            findProjects(extClsLdr, rootProjects, subprojects);
082            rootProject = LauncherSupport.createProjects(rootProjects.get(0),
083                subprojects, jdbldProps, commandLine);
084            CommandLine commandLine;
085            try {
086                commandLine = new DefaultParser().parse(baseOptions(), args);
087            } catch (ParseException e) {
088                throw new BuildException(e);
089            }
090            for (var arg : commandLine.getArgs()) {
091                var reqs = LauncherSupport.lookupCommand(rootProject, arg);
092                if (reqs.length == 0) {
093                    throw new BuildException("Unknown command: " + arg);
094                }
095                for (var req : reqs) {
096                    rootProject.get(req).collect(Collectors.toSet())
097                        .forEach(r -> System.out.println(r.toString()));
098                }
099            }
100            return null;
101        });
102    }
103
104    private ClassLoader addRuntimeExts(ClassLoader classloader) {
105        String[] coordinates = Arrays
106            .asList(jdbldProps.getProperty(RUNTIME_EXTENSIONS, "").split(","))
107            .stream()
108            .map(String::trim).filter(c -> !c.isBlank()).toArray(String[]::new);
109        if (coordinates.length == 0) {
110            return classloader;
111        }
112
113        // Resolve using maven repo
114        var cpUrls = resolveRequested(coordinates).mapMulti((jf, consumer) -> {
115            try {
116                consumer.accept(jf.path().toFile().toURI().toURL());
117            } catch (MalformedURLException e) {
118                log.log(Level.WARNING, e, () -> "Cannot convert " + jf
119                    + " to URL: " + e.getMessage());
120            }
121        }).toArray(URL[]::new);
122
123        // Return augmented classloader
124        return new URLClassLoader(cpUrls, classloader);
125    }
126
127    @SuppressWarnings({ "PMD.UseVarargs",
128        "PMD.AvoidInstantiatingObjectsInLoops" })
129    private Stream<JarFile> resolveRequested(String[] coordinates) {
130        ContextOverrides overrides = ContextOverrides.create()
131            .withUserSettings(true).build();
132        Runtime runtime = Runtimes.INSTANCE.getRuntime();
133        try (Context context = runtime.create(overrides)) {
134            CollectRequest collectRequest = new CollectRequest()
135                .setRepositories(context.remoteRepositories());
136            for (var coord : coordinates) {
137                collectRequest.addDependency(
138                    new Dependency(new DefaultArtifact(coord), "runtime"));
139            }
140
141            DependencyRequest dependencyRequest
142                = new DependencyRequest(collectRequest, null);
143            DependencyNode rootNode;
144            try {
145                rootNode = context.repositorySystem()
146                    .resolveDependencies(context.repositorySystemSession(),
147                        dependencyRequest)
148                    .getRoot();
149                PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
150                rootNode.accept(nlg);
151                List<DependencyNode> dependencyNodes = nlg.getNodes();
152                return dependencyNodes.stream()
153                    .filter(d -> d.getArtifact() != null)
154                    .map(d -> d.getArtifact().getFile().toPath())
155                    .map(p -> ResourceFactory
156                        .create(JarFileType, p));
157            } catch (DependencyResolutionException e) {
158                throw new BuildException(
159                    "Cannot resolve: " + e.getMessage(), e);
160            }
161        }
162    }
163
164    @Override
165    public <T extends Resource> Stream<T> provide(ResourceRequest<T> request) {
166        return unwrapBuildException(() -> {
167            // Provide requested resource, handling all exceptions here
168            var result = rootProject.get(request).toList();
169            return result.stream();
170        });
171    }
172
173    /// This main can be used to start the user's JDrupes Builder
174    /// project from an IDE for debugging purposes. It expects that
175    /// the JDrupes Builder project has already been compiled (typically
176    /// by the IDE) and is available on the classpath.
177    ///
178    /// @param args the arguments
179    ///
180    public static void main(String[] args) {
181        new DirectLauncher(Thread.currentThread().getContextClassLoader(),
182            args);
183    }
184}