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