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