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