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 eu.maveniverse.maven.mima.context.Context; 022import eu.maveniverse.maven.mima.context.ContextOverrides; 023import eu.maveniverse.maven.mima.context.Runtime; 024import eu.maveniverse.maven.mima.context.Runtimes; 025import java.net.MalformedURLException; 026import java.net.URL; 027import java.net.URLClassLoader; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.List; 031import java.util.logging.Level; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034import org.apache.commons.cli.CommandLine; 035import org.apache.commons.cli.DefaultParser; 036import org.apache.commons.cli.ParseException; 037import org.eclipse.aether.artifact.DefaultArtifact; 038import org.eclipse.aether.collection.CollectRequest; 039import org.eclipse.aether.graph.Dependency; 040import org.eclipse.aether.graph.DependencyNode; 041import org.eclipse.aether.resolution.DependencyRequest; 042import org.eclipse.aether.resolution.DependencyResolutionException; 043import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; 044import org.jdrupes.builder.api.BuildException; 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 org.jdrupes.builder.api.RootProject; 052import org.jdrupes.builder.core.LauncherSupport; 053import org.jdrupes.builder.java.JarFile; 054import static org.jdrupes.builder.java.JavaTypes.*; 055 056/// An implementation of a [Launcher] that expects that the JDrupes 057/// Builder project already been compiled and its classes are available 058/// on the classpath. 059/// 060public class DirectLauncher extends AbstractLauncher { 061 062 private static final String RUNTIME_EXTENSIONS = "runtimeExtensions"; 063 private RootProject rootProject; 064 065 /// Instantiates a new direct launcher. The classpath is scanned for 066 /// classes that implement [Project] but do not implement [Masked]. 067 /// One of these must also implement the [RootProject] interface. 068 /// The latter is instantiated and registered as root project with all 069 /// other classes found as direct sub projects. 070 /// 071 /// @param classloader the classloader 072 /// @param args the arguments 073 /// 074 @SuppressWarnings({ "PMD.UseVarargs", "PMD.SystemPrintln" }) 075 public DirectLauncher(ClassLoader classloader, String[] args) { 076 super(args); 077 unwrapBuildException(() -> { 078 final var extClsLdr = addRuntimeExts(classloader); 079 var rootProjects = new ArrayList<Class<? extends RootProject>>(); 080 var subprojects = new ArrayList<Class<? extends Project>>(); 081 findProjects(extClsLdr, rootProjects, subprojects); 082 rootProject = LauncherSupport.createProjects(rootProjects.get(0), 083 subprojects, jdbldProps, commandLine); 084 CommandLine commandLine; 085 try { 086 commandLine = new DefaultParser().parse(baseOptions(), args); 087 } catch (ParseException e) { 088 throw new BuildException(e); 089 } 090 for (var arg : commandLine.getArgs()) { 091 var reqs = LauncherSupport.lookupCommand(rootProject, arg); 092 if (reqs.length == 0) { 093 throw new BuildException("Unknown command: " + arg); 094 } 095 for (var req : reqs) { 096 rootProject.get(req).collect(Collectors.toSet()) 097 .forEach(r -> System.out.println(r.toString())); 098 } 099 } 100 return null; 101 }); 102 } 103 104 private ClassLoader addRuntimeExts(ClassLoader classloader) { 105 String[] coordinates = Arrays 106 .asList(jdbldProps.getProperty(RUNTIME_EXTENSIONS, "").split(",")) 107 .stream() 108 .map(String::trim).filter(c -> !c.isBlank()).toArray(String[]::new); 109 if (coordinates.length == 0) { 110 return classloader; 111 } 112 113 // Resolve using maven repo 114 var cpUrls = resolveRequested(coordinates).mapMulti((jf, consumer) -> { 115 try { 116 consumer.accept(jf.path().toFile().toURI().toURL()); 117 } catch (MalformedURLException e) { 118 log.log(Level.WARNING, e, () -> "Cannot convert " + jf 119 + " to URL: " + e.getMessage()); 120 } 121 }).toArray(URL[]::new); 122 123 // Return augmented classloader 124 return new URLClassLoader(cpUrls, classloader); 125 } 126 127 @SuppressWarnings({ "PMD.UseVarargs", 128 "PMD.AvoidInstantiatingObjectsInLoops" }) 129 private Stream<JarFile> resolveRequested(String[] coordinates) { 130 ContextOverrides overrides = ContextOverrides.create() 131 .withUserSettings(true).build(); 132 Runtime runtime = Runtimes.INSTANCE.getRuntime(); 133 try (Context context = runtime.create(overrides)) { 134 CollectRequest collectRequest = new CollectRequest() 135 .setRepositories(context.remoteRepositories()); 136 for (var coord : coordinates) { 137 collectRequest.addDependency( 138 new Dependency(new DefaultArtifact(coord), "runtime")); 139 } 140 141 DependencyRequest dependencyRequest 142 = new DependencyRequest(collectRequest, null); 143 DependencyNode rootNode; 144 try { 145 rootNode = context.repositorySystem() 146 .resolveDependencies(context.repositorySystemSession(), 147 dependencyRequest) 148 .getRoot(); 149 PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); 150 rootNode.accept(nlg); 151 List<DependencyNode> dependencyNodes = nlg.getNodes(); 152 return dependencyNodes.stream() 153 .filter(d -> d.getArtifact() != null) 154 .map(d -> d.getArtifact().getFile().toPath()) 155 .map(p -> ResourceFactory 156 .create(JarFileType, p)); 157 } catch (DependencyResolutionException e) { 158 throw new BuildException( 159 "Cannot resolve: " + e.getMessage(), e); 160 } 161 } 162 } 163 164 @Override 165 public <T extends Resource> Stream<T> provide(ResourceRequest<T> request) { 166 return unwrapBuildException(() -> { 167 // Provide requested resource, handling all exceptions here 168 var result = rootProject.get(request).toList(); 169 return result.stream(); 170 }); 171 } 172 173 /// This main can be used to start the user's JDrupes Builder 174 /// project from an IDE for debugging purposes. It expects that 175 /// the JDrupes Builder project has already been compiled (typically 176 /// by the IDE) and is available on the classpath. 177 /// 178 /// @param args the arguments 179 /// 180 public static void main(String[] args) { 181 new DirectLauncher(Thread.currentThread().getContextClassLoader(), 182 args); 183 } 184}