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}