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 java.io.IOException;
022import java.io.InputStream;
023import java.lang.reflect.Modifier;
024import java.net.URISyntaxException;
025import java.net.URL;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031import java.util.Properties;
032import java.util.concurrent.Callable;
033import java.util.logging.Level;
034import java.util.logging.LogManager;
035import java.util.logging.Logger;
036import java.util.stream.Collectors;
037import org.apache.commons.cli.CommandLine;
038import org.apache.commons.cli.DefaultParser;
039import org.apache.commons.cli.Option;
040import org.apache.commons.cli.Options;
041import org.apache.commons.cli.ParseException;
042import org.jdrupes.builder.api.BuildException;
043import org.jdrupes.builder.api.FileTree;
044import org.jdrupes.builder.api.Launcher;
045import org.jdrupes.builder.api.Masked;
046import org.jdrupes.builder.api.Project;
047import org.jdrupes.builder.api.ResourceFactory;
048import org.jdrupes.builder.api.RootProject;
049import org.jdrupes.builder.core.DefaultBuildContext;
050import static org.jdrupes.builder.java.JavaTypes.*;
051
052/// A default implementation of a [Launcher].
053///
054@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
055public abstract class AbstractLauncher implements Launcher {
056
057    /// The JDrupes Builder properties read from the file
058    /// `.jdbld.properties` in the root project.
059    @SuppressWarnings("PMD.FieldNamingConventions")
060    protected static final Properties jdbldProps;
061    /// The log.
062    protected final Logger log = Logger.getLogger(getClass().getName());
063    /// The command line.
064    protected final CommandLine commandLine;
065
066    static {
067        // Get builder configuration
068        Properties fallbacks = new Properties();
069        fallbacks.putAll(Map.of(DefaultBuildContext.JDBLD_DIRECTORY, "_jdbld"));
070        for (Path propsPath : List.of(
071            Path.of(System.getProperty("user.home"))
072                .resolve(".jdbld").resolve("jdbld.properties"),
073            Path.of("").toAbsolutePath().resolve(".jdbld.properties"))) {
074            try {
075                if (propsPath.toFile().canRead()) {
076                    fallbacks = new Properties(fallbacks);
077                    fallbacks.load(Files.newBufferedReader(propsPath));
078                }
079            } catch (IOException e) {
080                throw new BuildException(
081                    "Cannot read properties from " + propsPath, e);
082            }
083        }
084        jdbldProps = new Properties(fallbacks);
085
086        // Get logging configuration
087        InputStream props;
088        try {
089            props = Files.newInputStream(Path.of(
090                jdbldProps.getProperty(DefaultBuildContext.JDBLD_DIRECTORY),
091                "logging.properties"));
092        } catch (IOException e) {
093            props = BootstrapLauncher.class
094                .getResourceAsStream("logging.properties");
095        }
096        // Get logging properties from file and put them in effect
097        try (var from = props) {
098            LogManager.getLogManager().readConfiguration(from);
099        } catch (SecurityException | IOException e) {
100            e.printStackTrace(); // NOPMD
101        }
102    }
103
104    /// Instantiates a new abstract launcher.
105    ///
106    /// @param args the command line arguments
107    ///
108    @SuppressWarnings("PMD.UseVarargs")
109    public AbstractLauncher(String[] args) {
110        Options options = new Options();
111        options.addOption("B-x", true, "Exclude from project scan");
112        options.addOption(Option.builder("P").hasArgs().valueSeparator('=')
113            .desc("Property in form key=value").get());
114        try {
115            commandLine = new DefaultParser().parse(options, args);
116        } catch (ParseException e) {
117            throw new BuildException(e);
118        }
119
120        // Set properties from command line
121        jdbldProps.putAll(commandLine.getOptionProperties("P"));
122    }
123
124    /// Find projects. The classpath is scanned for classes that implement
125    /// [Project] but do not implement [Masked].
126    ///
127    /// @param clsLoader the cls loader
128    /// @param rootProjects classes that implement [RootProject]
129    /// @param subprojects classes that implement [Project] but not
130    /// [RootProject]
131    ///
132    @SuppressWarnings({ "unchecked", "PMD.AvoidLiteralsInIfCondition" })
133    protected void findProjects(ClassLoader clsLoader,
134            List<Class<? extends RootProject>> rootProjects,
135            List<Class<? extends Project>> subprojects) {
136        List<URL> classDirUrls;
137        try {
138            classDirUrls = Collections.list(clsLoader.getResources(""));
139        } catch (IOException e) {
140            throw new BuildException("Problem scanning classpath", e);
141        }
142        classDirUrls.parallelStream()
143            .filter(uri -> !"jar".equals(uri.getProtocol())).map(uri -> {
144                try {
145                    return Path.of(uri.toURI());
146                } catch (URISyntaxException e) {
147                    throw new BuildException("Problem scanning classpath", e);
148                }
149            })
150            .map(
151                p -> ResourceFactory.create(ClassTreeType, p, "**/*.class",
152                    false))
153            .flatMap(FileTree::entries).map(Path::toString)
154            .map(p -> p.substring(0, p.length() - 6).replace('/', '.'))
155            .map(cn -> {
156                try {
157                    return clsLoader.loadClass(cn);
158                } catch (ClassNotFoundException e) {
159                    throw new IllegalStateException(
160                        "Cannot load detected class", e);
161                }
162            }).forEach(cls -> {
163                if (!Masked.class.isAssignableFrom(cls) && !cls.isInterface()
164                    && !Modifier.isAbstract(cls.getModifiers())) {
165                    if (RootProject.class.isAssignableFrom(cls)) {
166                        rootProjects.add((Class<? extends RootProject>) cls);
167                    } else if (Project.class.isAssignableFrom(cls)) {
168                        subprojects.add((Class<? extends Project>) cls);
169                    }
170                }
171            });
172        if (rootProjects.isEmpty()) {
173            throw new BuildException("No project implements RootProject");
174        }
175        if (rootProjects.size() > 1) {
176            StringBuilder msg = new StringBuilder(50);
177            msg.append("More than one project implements RootProject: ")
178                .append(rootProjects.stream().map(Class::getName)
179                    .collect(Collectors.joining(", ")));
180            throw new BuildException(msg.toString());
181        }
182    }
183
184    /// A utility method that invokes the callable. If an exception
185    /// occurs during the invocation, it unwraps the causes until it
186    /// finds the root [BuildException], prints the message from this
187    /// exception and exits.
188    ///
189    /// @param <T> the generic type
190    /// @param todo the todo
191    /// @return the t
192    ///
193    @SuppressWarnings({ "PMD.DoNotTerminateVM",
194        "PMD.AvoidCatchingGenericException" })
195    protected final <T> T unwrapBuildException(Callable<T> todo) {
196        try {
197            return todo.call();
198        } catch (Exception e) {
199            Throwable checking = e;
200            Throwable cause = e;
201            BuildException bldEx = null;
202            while (checking != null) {
203                if (checking instanceof BuildException exc) {
204                    bldEx = exc;
205                    cause = exc.getCause();
206                }
207                checking = checking.getCause();
208            }
209            final var finalBldEx = bldEx;
210            if (bldEx == null) {
211                log.log(Level.SEVERE, e,
212                    () -> "Starting builder failed: " + e.getMessage());
213            } else if (cause == null) {
214                log.severe(() -> "Build failed: " + finalBldEx.getMessage());
215            } else {
216                log.log(Level.SEVERE, cause,
217                    () -> "Build failed: " + finalBldEx.getMessage());
218            }
219            System.exit(1);
220            return null;
221        }
222    }
223}