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}