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}