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.startup;
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.MalformedURLException;
027import java.net.URL;
028import java.net.URLClassLoader;
029import java.nio.file.Path;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.List;
033import java.util.Properties;
034import java.util.stream.Collectors;
035import java.util.stream.Stream;
036import org.apache.commons.cli.CommandLine;
037import org.apache.commons.cli.DefaultParser;
038import org.apache.commons.cli.ParseException;
039import org.eclipse.aether.artifact.DefaultArtifact;
040import org.eclipse.aether.collection.CollectRequest;
041import org.eclipse.aether.graph.Dependency;
042import org.eclipse.aether.graph.DependencyNode;
043import org.eclipse.aether.resolution.DependencyRequest;
044import org.eclipse.aether.resolution.DependencyResolutionException;
045import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator;
046import org.jdrupes.builder.api.BuildException;
047import org.jdrupes.builder.api.FaultAware;
048import org.jdrupes.builder.api.Launcher;
049import org.jdrupes.builder.api.Masked;
050import org.jdrupes.builder.api.Project;
051import org.jdrupes.builder.api.ResourceFactory;
052import org.jdrupes.builder.api.RootProject;
053import org.jdrupes.builder.core.LauncherSupport;
054import org.jdrupes.builder.java.JarFile;
055import static org.jdrupes.builder.java.JavaTypes.*;
056
057/// An implementation of a [Launcher] that launches the build configuration.
058/// It expects that the JDrupes Builder project has already been compiled
059/// and its classes are available on the classpath.
060///
061public class BuildProjectLauncher extends AbstractLauncher {
062
063    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
064    /// The JDrupes Builder properties read from the file
065    /// `.jdbld.properties` in the root project.
066    protected Properties jdbldProps;
067    /// The command line.
068    protected CommandLine commandLine;
069    private static final String RUNTIME_EXTENSIONS = "runtimeExtensions";
070    private final RootProject rootProject;
071
072    /// Instantiates a new direct launcher. The classpath is scanned for
073    /// classes that implement [Project] but do not implement [Masked].
074    /// One of these must also implement the [RootProject] interface.
075    /// The latter is instantiated and registered as root project with all
076    /// other classes found as direct sub projects.
077    ///
078    /// @param classloader the classloader
079    /// @param buildRoot the build root
080    /// @param args the arguments. Flags are processed in the constructor,
081    /// command line arguments are processed in [runCommands].
082    ///
083    @SuppressWarnings({ "PMD.UseVarargs",
084        "PMD.ConstructorCallsOverridableMethod" })
085    public BuildProjectLauncher(ClassLoader classloader, Path buildRoot,
086            String[] args) {
087        jdbldProps = propertiesFromFiles(buildRoot);
088        try {
089            commandLine = new DefaultParser().parse(baseOptions(), args);
090        } catch (ParseException e) {
091            configureLogging(buildRoot, jdbldProps);
092            throw new BuildException().cause(e);
093        }
094        addCliProperties(jdbldProps, commandLine);
095        configureLogging(buildRoot, jdbldProps);
096
097        final var extClsLdr = addRuntimeExts(classloader);
098        var rootProjects = new ArrayList<Class<? extends RootProject>>();
099        var subprojects = new ArrayList<Class<? extends Project>>();
100        findProjects(extClsLdr, rootProjects, subprojects);
101        rootProject = LauncherSupport.createProjects(buildRoot,
102            rootProjects.get(0), subprojects, jdbldProps, commandLine);
103    }
104
105    private ClassLoader addRuntimeExts(ClassLoader classloader) {
106        String[] coordinates = Arrays
107            .asList(jdbldProps.getProperty(RUNTIME_EXTENSIONS, "").split(","))
108            .stream()
109            .map(String::trim).filter(c -> !c.isBlank()).toArray(String[]::new);
110        if (coordinates.length == 0) {
111            return classloader;
112        }
113
114        // Resolve using maven repo
115        var cpUrls = resolveRequested(coordinates).mapMulti((jf, consumer) -> {
116            try {
117                consumer.accept(jf.path().toFile().toURI().toURL());
118            } catch (MalformedURLException e) {
119                logger.atWarning().withCause(e).log("Cannot convert %s to URL",
120                    jf);
121            }
122        }).toArray(URL[]::new);
123
124        // Return augmented classloader
125        return new URLClassLoader(cpUrls, classloader);
126    }
127
128    @SuppressWarnings({ "PMD.UseVarargs",
129        "PMD.AvoidInstantiatingObjectsInLoops" })
130    private Stream<JarFile> resolveRequested(String[] coordinates) {
131        ContextOverrides overrides = ContextOverrides.create()
132            .withUserSettings(true).build();
133        Runtime runtime = Runtimes.INSTANCE.getRuntime();
134        try (Context context = runtime.create(overrides)) {
135            CollectRequest collectRequest = new CollectRequest()
136                .setRepositories(context.remoteRepositories());
137            for (var coord : coordinates) {
138                collectRequest.addDependency(
139                    new Dependency(new DefaultArtifact(coord), "runtime"));
140            }
141
142            DependencyRequest dependencyRequest
143                = new DependencyRequest(collectRequest, null);
144            DependencyNode rootNode;
145            try {
146                rootNode = context.repositorySystem()
147                    .resolveDependencies(context.repositorySystemSession(),
148                        dependencyRequest)
149                    .getRoot();
150                PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
151                rootNode.accept(nlg);
152                List<DependencyNode> dependencyNodes = nlg.getNodes();
153                return dependencyNodes.stream()
154                    .filter(d -> d.getArtifact() != null)
155                    .map(d -> d.getArtifact().getFile().toPath())
156                    .map(p -> ResourceFactory
157                        .create(JarFileType, p));
158            } catch (DependencyResolutionException e) {
159                throw new BuildException("Cannot resolve: %s", e.getMessage())
160                    .cause(e);
161            }
162        }
163    }
164
165    @Override
166    public RootProject rootProject() {
167        return rootProject;
168    }
169
170    /// Execute the commands from the command line.
171    ///
172    /// @return true, if successful
173    ///
174    @SuppressWarnings({ "PMD.SystemPrintln", "PMD.AvoidLiteralsInIfCondition",
175        "PMD.AvoidInstantiatingObjectsInLoops" })
176    public boolean runCommands() {
177        for (var arg : commandLine.getArgs()) {
178            var parts = arg.split(":");
179            String resource = parts[parts.length - 1];
180            var cmdData = LauncherSupport.lookupCommand(rootProject, resource);
181            if (cmdData.requests().length == 0) {
182                throw new BuildException("Unknown command: %s", arg)
183                    .from(rootProject());
184            }
185            String pattern = cmdData.pattern();
186            if (parts.length > 1) {
187                pattern = parts[0];
188            }
189            for (var req : cmdData.requests()) {
190                if (!resources(rootProject().projects(pattern), req)
191                    // eliminate duplicates
192                    .collect(Collectors.toSet()).stream()
193                    .peek(r -> System.out.println(r.toString()))
194                    .map(r -> !(r instanceof FaultAware)
195                        || !((FaultAware) r).isFaulty())
196                    .reduce((r1, r2) -> r1 && r2).orElse(true)) {
197                    return false;
198                }
199            }
200        }
201        return true;
202    }
203
204    /// This main can be used to start the user's JDrupes Builder
205    /// project from an IDE for debugging purposes. It expects that
206    /// the JDrupes Builder project has already been compiled (typically
207    /// by the IDE) and is available on the classpath.
208    ///
209    /// @param args the arguments
210    ///
211    @SuppressWarnings("PMD.SystemPrintln")
212    public static void main(String[] args) {
213        try {
214            if (!reportBuildException(() -> new BuildProjectLauncher(
215                Thread.currentThread().getContextClassLoader(),
216                Path.of("").toAbsolutePath(), args).runCommands())) {
217                java.lang.Runtime.getRuntime().exit(1);
218            }
219        } catch (BuildException e) {
220            if (e.getCause() == null) {
221                logger.atSevere().log("Build failed: %s",
222                    formatter().summary(e));
223            } else {
224                logger.atSevere().withCause(e).log("Build failed: %s",
225                    formatter().summary(e));
226            }
227            System.out.println(formatter().summary(e));
228            java.lang.Runtime.getRuntime().exit(2);
229        }
230    }
231}