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