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.io.IOException; 023import java.io.InputStream; 024import java.lang.reflect.Modifier; 025import java.net.URISyntaxException; 026import java.net.URL; 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.List; 032import java.util.Map; 033import java.util.Properties; 034import java.util.concurrent.Callable; 035import java.util.concurrent.ConcurrentHashMap; 036import java.util.logging.LogManager; 037import java.util.stream.Collectors; 038import java.util.stream.Stream; 039import org.apache.commons.cli.CommandLine; 040import org.apache.commons.cli.Option; 041import org.apache.commons.cli.Options; 042import org.jdrupes.builder.api.BuildContext; 043import org.jdrupes.builder.api.BuildException; 044import org.jdrupes.builder.api.ConfigurationException; 045import org.jdrupes.builder.api.Launcher; 046import org.jdrupes.builder.api.Masked; 047import org.jdrupes.builder.api.Project; 048import org.jdrupes.builder.api.Resource; 049import org.jdrupes.builder.api.ResourceFactory; 050import org.jdrupes.builder.api.ResourceRequest; 051import static org.jdrupes.builder.api.ResourceType.*; 052import org.jdrupes.builder.api.RootProject; 053import org.jdrupes.builder.core.BuildExceptionFormatter; 054import org.jdrupes.builder.core.DefaultBuildContext; 055import org.jdrupes.builder.core.DefaultBuildExceptionFormatter; 056import org.jdrupes.builder.java.ClassTree; 057import static org.jdrupes.builder.java.JavaTypes.*; 058 059/// A default implementation of a [Launcher]. 060/// 061@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 062public abstract class AbstractLauncher implements Launcher { 063 064 /// The log. 065 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 066 @SuppressWarnings("PMD.FieldNamingConventions") 067 private static final BuildExceptionFormatter defaultFormatter 068 = new DefaultBuildExceptionFormatter(); 069 070 /// Initializes a new abstract launcher. 071 /// 072 protected AbstractLauncher() { 073 // Makes javadoc happy 074 } 075 076 /// Get the properties from the properties files in the user's home 077 /// directory and the build root directory. 078 /// 079 /// @param buildRoot the build root directory 080 /// @return the properties 081 /// 082 protected static Properties propertiesFromFiles(Path buildRoot) { 083 Properties fallbacks = new Properties(); 084 fallbacks.putAll(Map.of(BuildContext.JDBLD_DIRECTORY, "_jdbld", 085 BuildContext.JDBLD_COMMON_DIRECTORY, 086 Path.of(System.getProperty("user.home")).resolve(".jdbld") 087 .toString())); 088 for (Path propsPath : List.of( 089 Path.of(fallbacks.getProperty(BuildContext.JDBLD_COMMON_DIRECTORY)) 090 .resolve("jdbld.properties"), 091 buildRoot.resolve(".jdbld.properties"))) { 092 try { 093 if (propsPath.toFile().canRead()) { 094 fallbacks = new Properties(fallbacks); 095 fallbacks.load(Files.newBufferedReader(propsPath)); 096 } 097 } catch (IOException e) { 098 throw new BuildException().cause(e); 099 } 100 } 101 return new Properties(fallbacks); 102 } 103 104 /// Adds properties or overrides existing properties with those from 105 /// the command line. 106 /// 107 /// @param jdbldProps the jdbld props 108 /// @param commandLine the command line 109 /// 110 protected static void addCliProperties(Properties jdbldProps, 111 CommandLine commandLine) { 112 jdbldProps.putAll(commandLine.getOptionProperties("P")); 113 } 114 115 /// Configure the logging from logging properties found in 116 /// `DefaultBuildContext.JDBLD_DIRECTORY` resolved against `buildRoot`. 117 /// 118 /// @param buildRoot the build root 119 /// @param jdbldProps the jdbld properties 120 /// 121 protected static void configureLogging(Path buildRoot, 122 Properties jdbldProps) { 123 // Get logging configuration 124 InputStream props; 125 try { 126 props = Files.newInputStream(Path.of( 127 jdbldProps.getProperty(DefaultBuildContext.JDBLD_DIRECTORY), 128 "logging.properties")); 129 } catch (IOException e) { 130 props = BootstrapProjectLauncher.class 131 .getResourceAsStream("logging.properties"); 132 } 133 // Get logging properties from file and put them in effect 134 try (var from = props) { 135 LogManager.getLogManager().readConfiguration(from); 136 } catch (SecurityException | IOException e) { 137 e.printStackTrace(); // NOPMD 138 } 139 } 140 141 /// Return the handled options. 142 /// 143 /// @return the options 144 /// 145 protected final Options baseOptions() { 146 Options options = new Options(); 147 options.addOption("B-x", true, "Exclude from project scan"); 148 options.addOption(Option.builder("P").hasArgs().valueSeparator('=') 149 .desc("Property in form key=value").get()); 150 return options; 151 } 152 153 /// Find projects. The classpath is scanned for classes that implement 154 /// [Project] but do not implement [Masked]. 155 /// 156 /// @param clsLoader the cls loader 157 /// @param rootProjects classes that implement [RootProject] 158 /// @param subprojects classes that implement [Project] but not 159 /// [RootProject] 160 /// 161 @SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition" }) 162 protected void findProjects(ClassLoader clsLoader, 163 List<Class<? extends RootProject>> rootProjects, 164 List<Class<? extends Project>> subprojects) { 165 List<URL> classDirUrls; 166 try { 167 classDirUrls = Collections.list(clsLoader.getResources("")); 168 } catch (IOException e) { 169 throw new BuildException().cause(e); 170 } 171 Map<Path, List<Class<? extends RootProject>>> rootProjectMap 172 = new ConcurrentHashMap<>(); 173 classDirUrls.parallelStream() 174 .filter(uri -> !"jar".equals(uri.getProtocol())).map(uri -> { 175 try { 176 return Path.of(uri.toURI()); 177 } catch (URISyntaxException e) { 178 throw new BuildException().cause(e); 179 } 180 }).map(p -> ResourceFactory.create(ClassTreeType, p, "**/*.class", 181 false)) 182 .forEach(tree -> searchTree(clsLoader, rootProjectMap, subprojects, 183 tree)); 184 if (rootProjectMap.isEmpty()) { 185 throw new ConfigurationException() 186 .message("No project implements RootProject"); 187 } 188 if (rootProjectMap.size() > 1) { 189 StringBuilder msg = new StringBuilder(50); 190 msg.append("More than one class implements RootProject: ") 191 .append(rootProjectMap.entrySet().stream() 192 .map(e -> e.getValue().get(0).getName() + " (in " 193 + e.getKey() + ")") 194 .collect(Collectors.joining(", "))); 195 throw new ConfigurationException().message(msg.toString()); 196 } 197 rootProjects.addAll(rootProjectMap.values().iterator().next()); 198 } 199 200 @SuppressWarnings("unchecked") 201 private void searchTree(ClassLoader clsLoader, 202 Map<Path, List<Class<? extends RootProject>>> rootProjects, 203 List<Class<? extends Project>> subprojects, ClassTree tree) { 204 tree.entries().map(Path::toString) 205 .map(p -> p.substring(0, p.length() - 6).replace('/', '.')) 206 .map(cn -> { 207 try { 208 return clsLoader.loadClass(cn); 209 } catch (ClassNotFoundException e) { 210 throw new IllegalStateException( 211 "Cannot load detected class", e); 212 } 213 }).forEach(cls -> { 214 if (!Masked.class.isAssignableFrom(cls) 215 && !cls.isInterface() 216 && !Modifier.isAbstract(cls.getModifiers())) { 217 if (RootProject.class.isAssignableFrom(cls)) { 218 logger.atFine().log("Found root project: %s in %s", 219 cls, tree.root()); 220 rootProjects.computeIfAbsent(tree.root(), 221 _ -> new ArrayList<>()) 222 .add((Class<? extends RootProject>) cls); 223 } else if (Project.class.isAssignableFrom(cls)) { 224 logger.atFiner().log("Found sub project: %s in %s", 225 cls, tree.root()); 226 subprojects.add((Class<? extends Project>) cls); 227 } 228 } 229 }); 230 } 231 232 @Override 233 public <T extends Resource> Stream<T> resources(Stream<Project> projects, 234 ResourceRequest<T> request) { 235 var result = reportBuildException(() -> projects 236 .map(p -> rootProject().context().resources(p, request)) 237 .flatMap(r -> r).toList().stream()); 238 if (request.isFor(CleanlinessType)) { 239 regenerateRootProject(); 240 } 241 return result; 242 } 243 244 /// A utility method for reliably reporting problems as [BuildException]s. 245 /// It invokes the callable. If a Throwable occurs, it unwraps the causes 246 /// until it finds the root [BuildException] and rethrows it. Any other 247 /// [Throwable] is wrapped in a new [BuildException] which is then thrown. 248 /// 249 /// Effectively, the method thus either returns the requested result 250 /// or a [BuildException]. 251 /// 252 /// @param <T> the generic type 253 /// @param todo the todo 254 /// @return the result 255 /// 256 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 257 "PMD.PreserveStackTrace" }) 258 public static final <T> T reportBuildException(Callable<T> todo) { 259 try { 260 return todo.call(); 261 } catch (Throwable thrown) { 262 Throwable checking = thrown; 263 BuildException foundBldEx = null; 264 while (checking != null) { 265 if (checking instanceof BuildException exc) { 266 foundBldEx = exc; 267 } 268 checking = checking.getCause(); 269 } 270 if (foundBldEx != null) { 271 throw foundBldEx; 272 } 273 throw new BuildException().cause(thrown); 274 } 275 } 276 277 /// Return the default formatter for build exceptions. 278 /// 279 /// @return the builds the exception formatter 280 /// 281 public static BuildExceptionFormatter formatter() { 282 return defaultFormatter; 283 } 284 285}