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}