001/* 002 * JDrupes Builder 003 * Copyright (C) 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.java; 020 021import com.google.common.flogger.FluentLogger; 022import static com.google.common.flogger.LazyArgs.*; 023import io.vavr.control.Option; 024import io.vavr.control.Try; 025import java.io.File; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.OutputStream; 029import java.lang.ProcessBuilder.Redirect; 030import java.nio.file.Path; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.List; 036import java.util.Objects; 037import java.util.jar.Attributes; 038import java.util.jar.Manifest; 039import java.util.stream.Collectors; 040import java.util.stream.Stream; 041import org.jdrupes.builder.api.BuildException; 042import org.jdrupes.builder.api.ConfigurationException; 043import org.jdrupes.builder.api.ExecResult; 044import org.jdrupes.builder.api.FileResource; 045import org.jdrupes.builder.api.FileTree; 046import org.jdrupes.builder.api.Project; 047import org.jdrupes.builder.api.Renamable; 048import org.jdrupes.builder.api.RequiredResourceSupport; 049import org.jdrupes.builder.api.Resource; 050import org.jdrupes.builder.api.ResourceProvider; 051import org.jdrupes.builder.api.ResourceRequest; 052import org.jdrupes.builder.api.ResourceRetriever; 053import org.jdrupes.builder.api.ResourceType; 054import static org.jdrupes.builder.api.ResourceType.*; 055import org.jdrupes.builder.api.Resources; 056import org.jdrupes.builder.core.AbstractProvider; 057import org.jdrupes.builder.core.StreamCollector; 058import static org.jdrupes.builder.java.JavaTypes.*; 059 060/// A provider for [execution results][ExecResult]s from invoking a JVM. 061/// 062/// The working directory is the project directory. 063/// 064public class JavaExecutor extends AbstractProvider 065 implements ResourceRetriever, Renamable, RequiredResourceSupport { 066 067 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 068 private final Project project; 069 private final StreamCollector<Resource> requiredResources 070 = new StreamCollector<>(false); 071 private final StreamCollector<ResourceProvider> providers 072 = StreamCollector.cached(); 073 private String mainClass; 074 private final List<String> arguments = new ArrayList<>(); 075 076 /// Initializes a new java executor. 077 /// 078 /// @param project the project 079 /// 080 public JavaExecutor(Project project) { 081 this.project = project; 082 rename(JavaExecutor.class.getSimpleName() + " in " + project); 083 } 084 085 @Override 086 public JavaExecutor name(String name) { 087 rename(name); 088 return this; 089 } 090 091 @Override 092 public JavaExecutor required(Stream<? extends Resource> resources) { 093 requiredResources.add(resources); 094 return this; 095 } 096 097 @Override 098 public JavaExecutor required(Path root, String pattern) { 099 requiredResources 100 .add(Stream.of(FileTree.of(project, root, pattern))); 101 return this; 102 } 103 104 @Override 105 public JavaExecutor required(Path root) { 106 requiredResources.add( 107 Stream.of(FileResource.of(project.directory().resolve(root)))); 108 return this; 109 } 110 111 /// Additionally uses the given providers for building the classpath. 112 /// 113 /// @param providers the providers 114 /// @return the jar generator 115 /// 116 @Override 117 public JavaExecutor addFrom(ResourceProvider... providers) { 118 addFrom(Stream.of(providers)); 119 return this; 120 } 121 122 /// Additionally uses the given providers for building the classpath. 123 /// 124 /// @param providers the providers 125 /// @return the jar generator 126 /// 127 @Override 128 public JavaExecutor addFrom(Stream<ResourceProvider> providers) { 129 this.providers.add(providers.filter(p -> !p.equals(this))); 130 return this; 131 } 132 133 /// Returns the main class. 134 /// 135 /// @return the main class 136 /// 137 public String mainClass() { 138 return mainClass; 139 } 140 141 /// Sets the main class. 142 /// 143 /// @param mainClass the new main class 144 /// @return the jar generator for method chaining 145 /// 146 public JavaExecutor mainClass(String mainClass) { 147 this.mainClass = Objects.requireNonNull(mainClass); 148 return this; 149 } 150 151 /// Add the given arguments. 152 /// 153 /// @param args the arguments 154 /// @return the npm executor 155 /// 156 public JavaExecutor args(String... args) { 157 arguments.addAll(Arrays.asList(args)); 158 return this; 159 } 160 161 @Override 162 protected <T extends Resource> Collection<T> 163 doProvide(ResourceRequest<T> requested) { 164 if (!requested.accepts(ExecResultType) 165 || requested.name().map(n -> !n.equals(name())).orElse(false)) { 166 return Collections.emptyList(); 167 } 168 169 // Make sure that the required resources are retrieved and exist 170 var required = Resources.of(new ResourceType<Resources<Resource>>() {}); 171 required.addAll(requiredResources.stream()); 172 173 // Collect the classpath and check mainClass. 174 var cpResources = Resources.of(ClasspathType) 175 .addAll(providers.stream() 176 .map(p -> p.resources(of(ClasspathElementType).usingAll())) 177 // Terminate to trigger all future stream evaluations before 178 // starting to process the results. 179 .toList().stream().flatMap(s -> s)); 180 logger.atFiner().log("Executing with classpath %s", 181 lazy(() -> cpResources.stream().map(e -> e.toPath().toString()) 182 .collect(Collectors.joining(File.pathSeparator)))); 183 if (mainClass == null) { 184 findMainClass(cpResources); 185 } 186 if (mainClass == null) { 187 throw new ConfigurationException().from(this).message( 188 "No main class defined for %s", name()); 189 } 190 191 // Build command 192 List<String> command = new ArrayList<>(List.of( 193 System.getProperty("java.home") + "/bin/java", 194 "-cp", cpResources.stream().map(e -> e.toPath().toString()) 195 .collect(Collectors.joining(File.pathSeparator)), 196 mainClass)); 197 command.addAll(arguments); 198 logger.atInfo().log("Executing %s", 199 command.stream().collect(Collectors.joining(" "))); 200 201 ProcessBuilder processBuilder = new ProcessBuilder(command) 202 .directory(project.directory().toFile()) 203 .redirectInput(Redirect.INHERIT); 204 try { 205 Process process = processBuilder.start(); 206 copyData(process.getInputStream(), context().out()); 207 copyData(process.getErrorStream(), context().error()); 208 var execResult 209 = ExecResult.of(this, mainClass, process.waitFor()); 210 if (execResult.exitValue() != 0) { 211 execResult.setFaulty(); 212 } 213 @SuppressWarnings("unchecked") 214 var result = (Collection<T>) List.of(execResult); 215 return result; 216 } catch (IOException | InterruptedException e) { 217 throw new BuildException().from(this).cause(e); 218 } 219 } 220 221 private void findMainClass(Resources<ClasspathElement> cpResources) { 222 vavrStream(cpResources).filter(cpe -> cpe instanceof JarFile) 223 .map(JarFile.class::cast).map(cpe -> Try.withResources( 224 () -> new java.util.jar.JarFile(cpe.path().toFile())) 225 .of(jar -> Try.of(jar::getManifest).toOption() 226 .flatMap(Option::of).map(Manifest::getMainAttributes) 227 .flatMap(a -> Option 228 .of(a.getValue(Attributes.Name.MAIN_CLASS)))) 229 .onFailure(e -> logger.atWarning().withCause(e).log( 230 "Problem reading %s", cpe)) 231 .toOption().flatMap(s -> s)) 232 .flatMap(Option::toStream).headOption().peek(mc -> mainClass = mc); 233 } 234 235 private void copyData(InputStream source, OutputStream sink) { 236 Thread.startVirtualThread(() -> { 237 try (source) { 238 source.transferTo(sink); 239 } catch (IOException e) { // NOPMD 240 } 241 }); 242 } 243 244 /// To string. 245 /// 246 /// @return the string 247 /// 248 @Override 249 public String toString() { 250 return super.toString() + "[" + project.name() + "]"; 251 } 252}