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