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}