001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 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.core;
020
021import com.google.common.flogger.FluentLogger;
022import static com.google.common.flogger.LazyArgs.lazy;
023import java.io.PrintStream;
024import java.nio.file.Path;
025import java.util.Collection;
026import java.util.EnumSet;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Properties;
030import java.util.concurrent.CompletableFuture;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Executors;
033import java.util.concurrent.atomic.AtomicBoolean;
034import java.util.stream.Collectors;
035import java.util.stream.Stream;
036import org.apache.commons.cli.CommandLine;
037import org.jdrupes.builder.api.BuildContext;
038import org.jdrupes.builder.api.BuildException;
039import org.jdrupes.builder.api.ConfigurationException;
040import org.jdrupes.builder.api.Intent;
041import org.jdrupes.builder.api.Project;
042import org.jdrupes.builder.api.Resource;
043import org.jdrupes.builder.api.ResourceProvider;
044import org.jdrupes.builder.api.ResourceRequest;
045import static org.jdrupes.builder.api.ResourceType.CleanlinessType;
046import org.jdrupes.builder.api.StatusLine;
047import org.jdrupes.builder.core.console.SplitConsole;
048
049/// A context for building.
050///
051public class DefaultBuildContext implements BuildContext {
052
053    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
054    @SuppressWarnings("PMD.FieldNamingConventions")
055    private static final ScopedValue<AtomicBoolean> providerInvocationAllowed
056        = ScopedValue.newInstance();
057    private final FutureStreamCache cache;
058    private ExecutorService executor
059        = Executors.newVirtualThreadPerTaskExecutor();
060    private final ClassLoader classLoader;
061    private final Path buildRoot;
062    private final Properties jdbldProperties;
063    private final CommandLine commandLine;
064    private final AwaitableCounter executingFutureStreams
065        = new AwaitableCounter();
066    private final SplitConsole console;
067    private final CompletableFuture<AbstractRootProject> buildProject
068        = new CompletableFuture<>();
069    @SuppressWarnings("PMD.FieldNamingConventions")
070    private static final ScopedValue<RequestChainLink> requestChainEnd
071        = ScopedValue.newInstance();
072
073    static {
074        ScopedValueContext.add(requestChainEnd);
075    }
076
077    /// A link in the call chain.
078    ///
079    /// @param previous the previous
080    /// @param invocation the invocation
081    ///
082    public record RequestChainLink(RequestChainLink previous,
083            ProviderInvocation<?> invocation) {
084    }
085
086    /// Initializes a new default build context. By default, the build
087    /// represented by this context uses a virtual thread per task executor.
088    ///
089    /// @param classLoader the class loader
090    /// @param buildRoot the build root
091    /// @param jdbldProperties the jdbld properties
092    /// @param commandLine the command line
093    ///
094    /* default */ DefaultBuildContext(ClassLoader classLoader, Path buildRoot,
095            Properties jdbldProperties, CommandLine commandLine) {
096        this.classLoader = classLoader;
097        this.buildRoot = buildRoot;
098        this.jdbldProperties = jdbldProperties;
099        this.commandLine = commandLine;
100        cache = new FutureStreamCache();
101        console = SplitConsole.open();
102    }
103
104    /// Returns the executor service used by this build to create futures.
105    ///
106    /// @return the executor service
107    ///
108    public ExecutorService executor() {
109        return executor;
110    }
111
112    /// Sets the executor service used by this build to create futures.
113    ///
114    /// @param executor the executor
115    ///
116    public void executor(ExecutorService executor) {
117        this.executor = executor;
118    }
119
120    /// Executing future streams.
121    ///
122    /// @return the awaitable counter
123    ///
124    public AwaitableCounter executingFutureStreams() {
125        return executingFutureStreams;
126    }
127
128    /// Returns the build root.
129    ///
130    /// @return the path
131    ///
132    public Path buildRoot() {
133        return buildRoot;
134    }
135
136    @Override
137    public ClassLoader classLoader() {
138        return classLoader;
139    }
140
141    @Override
142    public CommandLine commandLine() {
143        return commandLine;
144    }
145
146    @Override
147    public String property(String name, String defaultValue) {
148        return jdbldProperties.getProperty(name,
149            defaultValue);
150    }
151
152    /// Start request chain.
153    ///
154    /// @param carriers the carriers
155    /// @return the scoped value. carrier
156    ///
157    public ScopedValue.Carrier startRequestChain(ScopedValue.Carrier carriers) {
158        if (requestChainEnd.isBound()) {
159            throw new ConfigurationException()
160                .message("Request chain is already bound.");
161        }
162        return carriers.where(requestChainEnd,
163            new RequestChainLink(null, ProviderInvocation.LAUNCH));
164    }
165
166    /// Return a carrier with this context available from [#context] and
167    /// the provider invocation allowed flag set.
168    ///
169    /// @param carrier the carrier
170    /// @return the augmented carrier
171    ///
172    /* default */ ScopedValue.Carrier inScopeForProviderCall() {
173        return ScopedValue
174            .where(providerInvocationAllowed, new AtomicBoolean(true));
175    }
176
177    /* default */ SplitConsole console() {
178        return console;
179    }
180
181    @Override
182    public StatusLine statusLine() {
183        return FutureStream.statusLine.orElse(SplitConsole.nullStatusLine());
184    }
185
186    @Override
187    public PrintStream out() {
188        return console().out();
189    }
190
191    @Override
192    public PrintStream error() {
193        return console().err();
194    }
195
196    @Override
197    public <T extends Resource> Stream<T> resources(ResourceProvider provider,
198            ResourceRequest<T> request) {
199        // Normalize request, non-project providers don't get intends
200        var invocation = new ProviderInvocation<>(provider,
201            provider instanceof Project || request.uses().isEmpty() ? request
202                : request.using(EnumSet.noneOf(Intent.class)));
203        return inScopeForProviderCall()
204            .call(() -> inResourcesContext(invocation));
205    }
206
207    @SuppressWarnings({ "PMD.AvoidSynchronizedStatement" })
208    private <T extends Resource> Stream<T> inResourcesContext(
209            ProviderInvocation<T> invocation) {
210        if (invocation.provider() instanceof Project) {
211            // As a project's provide only delegates to other providers
212            // it is inefficient to invoke it asynchronously. Nevertheless,
213            // SPI must be invoked lazily.
214            var snapshot = ScopedValueContext.snapshot();
215            return LazyCollectionStream.of(
216                () -> snapshot.where(providerInvocationAllowed,
217                    new AtomicBoolean(true)).call(() -> invokeSpi(invocation)));
218        }
219        if (!invocation.request().type().equals(CleanlinessType)) {
220            return cache.computeIfAbsent(invocation,
221                k -> new FutureStream<T>(k)).stream();
222        }
223
224        // Special handling for cleanliness. Clean one by one...
225        synchronized (executor) {
226            // Await completion of all generating threads
227            try {
228                executingFutureStreams().await(0);
229            } catch (InterruptedException e) {
230                throw new BuildException().cause(e);
231            }
232        }
233        var result = invokeSpi(invocation).stream();
234        // Purge cached results from provider
235        cache.purge(invocation.provider());
236        return result;
237    }
238
239    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
240    private <T extends Resource> Collection<T>
241            invokeSpi(ProviderInvocation<T> invocation) {
242        return ScopedValue.where(requestChainEnd, new RequestChainLink(
243            requestChainEnd.orElseThrow(() -> new ConfigurationException()
244                .cause(new IllegalStateException())
245                .message("No request chain end")),
246            invocation)).call(() -> {
247                logger.atFinest().log("Request chain: %s",
248                    lazy(() -> requestChain()
249                        .stream().map(ProviderInvocation::toString)
250                        .collect(Collectors.joining(" ≪ "))));
251                var prev = requestChainEnd.get().previous;
252                while (prev != null) {
253                    if (invocation.equals(prev.invocation())) {
254                        throw new BuildException().message("Request loop: %s",
255                            requestChain().stream()
256                                .map(ProviderInvocation::toString)
257                                .collect(Collectors.joining(" ≪ ")));
258                    }
259                    prev = prev.previous;
260                }
261                return ((AbstractProvider) invocation.provider()).toSpi()
262                    .provide(invocation.request());
263            });
264    }
265
266    /* default */ List<ProviderInvocation<?>> requestChain() {
267        var cur = requestChainEnd.isBound() ? requestChainEnd.get() : null;
268        List<ProviderInvocation<?>> result = new LinkedList<>();
269        while (cur != null) {
270            result.add(cur.invocation);
271            cur = cur.previous;
272        }
273        return result;
274    }
275
276    /// Checks if is provider invocation is allowed. Clears the
277    /// allowed flag to also detect nested invocations.
278    ///
279    /// @return true, if is provider invocation allowed
280    ///
281    public static boolean isProviderInvocationAllowed() {
282        return providerInvocationAllowed.isBound()
283            && providerInvocationAllowed.get().getAndSet(false);
284    }
285
286    @Override
287    public void close() {
288        executor.shutdownNow();
289        console.close();
290    }
291
292    /* default */ CompletableFuture<AbstractRootProject> buildProject() {
293        return buildProject;
294    }
295}