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.util.List; 031import java.util.Objects; 032import java.util.jar.Attributes; 033import java.util.jar.Manifest; 034import java.util.stream.Collectors; 035import java.util.stream.Stream; 036import org.jdrupes.builder.api.BuildException; 037import org.jdrupes.builder.api.ConfigurationException; 038import org.jdrupes.builder.api.ExecResult; 039import org.jdrupes.builder.api.Project; 040import org.jdrupes.builder.api.Renamable; 041import org.jdrupes.builder.api.Resource; 042import org.jdrupes.builder.api.ResourceProvider; 043import org.jdrupes.builder.api.ResourceRequest; 044import org.jdrupes.builder.api.ResourceRetriever; 045import static org.jdrupes.builder.api.ResourceType.*; 046import org.jdrupes.builder.api.Resources; 047import org.jdrupes.builder.core.AbstractProvider; 048import org.jdrupes.builder.core.StreamCollector; 049import static org.jdrupes.builder.java.JavaTypes.*; 050 051/// A provider for [execution results][ExecResult]s from invoking a JVM. 052/// 053public class JavaExecutor extends AbstractProvider 054 implements ResourceRetriever, Renamable { 055 056 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 057 private final StreamCollector<ResourceProvider> providers 058 = StreamCollector.cached(); 059 private String mainClass; 060 061 /// Initializes a new java executor. 062 /// 063 /// @param project the project 064 /// 065 public JavaExecutor(Project project) { 066 rename(JavaExecutor.class.getSimpleName() + " in " + project); 067 } 068 069 @Override 070 public JavaExecutor name(String name) { 071 rename(name); 072 return this; 073 } 074 075 /// Additionally uses the given providers for obtaining contents for the 076 /// jar. 077 /// 078 /// @param providers the providers 079 /// @return the jar generator 080 /// 081 @Override 082 public JavaExecutor addFrom(ResourceProvider... providers) { 083 addFrom(Stream.of(providers)); 084 return this; 085 } 086 087 /// Additionally uses the given providers for obtaining contents for the 088 /// jar. 089 /// 090 /// @param providers the providers 091 /// @return the jar generator 092 /// 093 @Override 094 public JavaExecutor addFrom(Stream<ResourceProvider> providers) { 095 this.providers.add(providers.filter(p -> !p.equals(this))); 096 return this; 097 } 098 099 /// Returns the main class. 100 /// 101 /// @return the main class 102 /// 103 public String mainClass() { 104 return mainClass; 105 } 106 107 /// Sets the main class. 108 /// 109 /// @param mainClass the new main class 110 /// @return the jar generator for method chaining 111 /// 112 public JavaExecutor mainClass(String mainClass) { 113 this.mainClass = Objects.requireNonNull(mainClass); 114 return this; 115 } 116 117 @Override 118 protected <T extends Resource> Stream<T> 119 doProvide(ResourceRequest<T> requested) { 120 if (!requested.accepts(ExecResultType) 121 || requested.name().map(n -> !n.equals(name())).orElse(false)) { 122 return Stream.empty(); 123 } 124 125 // Collect the classpath and check mainClass. 126 var cpResources = Resources.of(ClasspathType) 127 .addAll(providers.stream() 128 .map(p -> p.resources(of(ClasspathElementType).usingAll())) 129 .flatMap(s -> s)); 130 logger.atFiner().log("Executing with classpath %s", 131 lazy(() -> cpResources.stream().map(e -> e.toPath().toString()) 132 .collect(Collectors.joining(File.pathSeparator)))); 133 if (mainClass == null) { 134 findMainClass(cpResources); 135 } 136 if (mainClass == null) { 137 throw new ConfigurationException().from(this).message( 138 "No main class defined for %s", name()); 139 } 140 141 // Build command 142 List<String> command = List.of( 143 System.getProperty("java.home") + "/bin/java", 144 "-cp", cpResources.stream().map(e -> e.toPath().toString()) 145 .collect(Collectors.joining(File.pathSeparator)), 146 mainClass); 147 148 ProcessBuilder processBuilder = new ProcessBuilder(command); 149 processBuilder.redirectInput(Redirect.INHERIT); 150 try { 151 Process process = processBuilder.start(); 152 copyData(process.getInputStream(), context().out()); 153 copyData(process.getErrorStream(), context().error()); 154 var execResult 155 = ExecResult.of(this, mainClass, process.waitFor()); 156 if (execResult.exitValue() != 0) { 157 execResult.setFaulty(); 158 } 159 @SuppressWarnings("unchecked") 160 var result = (Stream<T>) Stream.of(execResult); 161 return result; 162 } catch (IOException | InterruptedException e) { 163 throw new BuildException().from(this).cause(e); 164 } 165 } 166 167 private void findMainClass(Resources<ClasspathElement> cpResources) { 168 vavrStream(cpResources).filter(cpe -> cpe instanceof JarFile) 169 .map(JarFile.class::cast).map(cpe -> Try.withResources( 170 () -> new java.util.jar.JarFile(cpe.path().toFile())) 171 .of(jar -> Try.of(jar::getManifest).toOption() 172 .flatMap(Option::of).map(Manifest::getMainAttributes) 173 .flatMap(a -> Option 174 .of(a.getValue(Attributes.Name.MAIN_CLASS)))) 175 .onFailure(e -> logger.atWarning().withCause(e).log( 176 "Problem reading %s", cpe)) 177 .toOption().flatMap(s -> s)) 178 .flatMap(Option::toStream).headOption().peek(mc -> mainClass = mc); 179 } 180 181 private void copyData(InputStream source, OutputStream sink) { 182 Thread.startVirtualThread(() -> { 183 try (source) { 184 source.transferTo(sink); 185 } catch (IOException e) { // NOPMD 186 } 187 }); 188 } 189}