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        try {
111            commandLine = new DefaultParser().parse(baseOptions(), args);
112        } catch (ParseException e) {
113            throw new BuildException(e);
114        }
115
116        // Set properties from command line
117        jdbldProps.putAll(commandLine.getOptionProperties("P"));
118    }
119
120    /// Return the handled options.
121    ///
122    /// @return the options
123    ///
124    protected final Options baseOptions() {
125        Options options = new Options();
126        options.addOption("B-x", true, "Exclude from project scan");
127        options.addOption(Option.builder("P").hasArgs().valueSeparator('=')
128            .desc("Property in form key=value").get());
129        return options;
130    }
131
132    /// Find projects. The classpath is scanned for classes that implement
133    /// [Project] but do not implement [Masked].
134    ///
135    /// @param clsLoader the cls loader
136    /// @param rootProjects classes that implement [RootProject]
137    /// @param subprojects classes that implement [Project] but not
138    /// [RootProject]
139    ///
140    @SuppressWarnings({ "unchecked", "PMD.AvoidLiteralsInIfCondition" })
141    protected void findProjects(ClassLoader clsLoader,
142            List<Class<? extends RootProject>> rootProjects,
143            List<Class<? extends Project>> subprojects) {
144        List<URL> classDirUrls;
145        try {
146            classDirUrls = Collections.list(clsLoader.getResources(""));
147        } catch (IOException e) {
148            throw new BuildException("Problem scanning classpath", e);
149        }
150        classDirUrls.parallelStream()
151            .filter(uri -> !"jar".equals(uri.getProtocol())).map(uri -> {
152                try {
153                    return Path.of(uri.toURI());
154                } catch (URISyntaxException e) {
155                    throw new BuildException("Problem scanning classpath", e);
156                }
157            })
158            .map(
159                p -> ResourceFactory.create(ClassTreeType, p, "**/*.class",
160                    false))
161            .flatMap(FileTree::entries).map(Path::toString)
162            .map(p -> p.substring(0, p.length() - 6).replace('/', '.'))
163            .map(cn -> {
164                try {
165                    return clsLoader.loadClass(cn);
166                } catch (ClassNotFoundException e) {
167                    throw new IllegalStateException(
168                        "Cannot load detected class", e);
169                }
170            }).forEach(cls -> {
171                if (!Masked.class.isAssignableFrom(cls) && !cls.isInterface()
172                    && !Modifier.isAbstract(cls.getModifiers())) {
173                    if (RootProject.class.isAssignableFrom(cls)) {
174                        rootProjects.add((Class<? extends RootProject>) cls);
175                    } else if (Project.class.isAssignableFrom(cls)) {
176                        subprojects.add((Class<? extends Project>) cls);
177                    }
178                }
179            });
180        if (rootProjects.isEmpty()) {
181            throw new BuildException("No project implements RootProject");
182        }
183        if (rootProjects.size() > 1) {
184            StringBuilder msg = new StringBuilder(50);
185            msg.append("More than one project implements RootProject: ")
186                .append(rootProjects.stream().map(Class::getName)
187                    .collect(Collectors.joining(", ")));
188            throw new BuildException(msg.toString());
189        }
190    }
191
192    /// A utility method that invokes the callable. If an exception
193    /// occurs during the invocation, it unwraps the causes until it
194    /// finds the root [BuildException], prints the message from this
195    /// exception and exits.
196    ///
197    /// @param <T> the generic type
198    /// @param todo the todo
199    /// @return the t
200    ///
201    @SuppressWarnings({ "PMD.DoNotTerminateVM",
202        "PMD.AvoidCatchingGenericException" })
203    protected final <T> T unwrapBuildException(Callable<T> todo) {
204        try {
205            return todo.call();
206        } catch (Exception e) {
207            Throwable checking = e;
208            Throwable cause = e;
209            BuildException bldEx = null;
210            while (checking != null) {
211                if (checking instanceof BuildException exc) {
212                    bldEx = exc;
213                    cause = exc.getCause();
214                }
215                checking = checking.getCause();
216            }
217            final var finalBldEx = bldEx;
218            if (bldEx == null) {
219                log.log(Level.SEVERE, e,
220                    () -> "Starting builder failed: " + e.getMessage());
221            } else if (cause == null) {
222                log.severe(() -> "Build failed: " + finalBldEx.getMessage());
223            } else {
224                log.log(Level.SEVERE, cause,
225                    () -> "Build failed: " + finalBldEx.getMessage());
226            }
227            System.exit(1);
228            return null;
229        }
230    }
231}