001/*
002 * JDrupes Builder
003 * Copyright (C) 2025 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 java.io.PrintStream;
022import java.nio.file.Path;
023import java.util.Arrays;
024import java.util.EnumSet;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Properties;
030import java.util.concurrent.CompletableFuture;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ExecutorService;
033import java.util.concurrent.Executors;
034import java.util.concurrent.Future;
035import java.util.concurrent.atomic.AtomicBoolean;
036import java.util.stream.Stream;
037import org.apache.commons.cli.CommandLine;
038import org.jdrupes.builder.api.BuildContext;
039import org.jdrupes.builder.api.BuildException;
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.RootProject;
047import org.jdrupes.builder.api.StatusLine;
048import org.jdrupes.builder.core.console.SplitConsole;
049
050/// A context for building.
051///
052@SuppressWarnings({ "PMD.CouplingBetweenObjects", "PMD.TooManyMethods" })
053public class DefaultBuildContext implements BuildContext {
054
055    @SuppressWarnings("PMD.FieldNamingConventions")
056    private static final ScopedValue<AtomicBoolean> providerInvocationAllowed
057        = ScopedValue.newInstance();
058    private final FutureStreamCache cache;
059    private ExecutorService executor
060        = Executors.newVirtualThreadPerTaskExecutor();
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    @SuppressWarnings("PMD.FieldNamingConventions")
068    private static final ScopedValue<
069            DefaultBuildContext> scopedBuildContext = ScopedValue.newInstance();
070    private final CompletableFuture<AbstractRootProject> buildProject
071        = new CompletableFuture<>();
072    @SuppressWarnings("PMD.FieldNamingConventions")
073    private static final ScopedValue<CallChainLink> callChainEnd
074        = ScopedValue.newInstance();
075    private final Map<ProviderInvocation<?>, CallChainLink> callChains
076        = new ConcurrentHashMap<>();
077
078    private record CallChainLink(CallChainLink previous,
079            ProviderInvocation<?> invocation) {
080    }
081
082    public class ContextScoped {
083        private CallChainLink callChainEnd;
084    }
085
086    /// Instantiates a new default build. By default, the build uses
087    /// a virtual thread per task executor.
088    ///
089    /* default */ DefaultBuildContext(Path buildRoot,
090            Properties jdbldProperties, CommandLine commandLine) {
091        this.buildRoot = buildRoot;
092        this.jdbldProperties = jdbldProperties;
093        this.commandLine = commandLine;
094        cache = new FutureStreamCache();
095        console = SplitConsole.open();
096    }
097
098    /// Returns the executor service used by this build to create futures.
099    ///
100    /// @return the executor service
101    ///
102    public ExecutorService executor() {
103        return executor;
104    }
105
106    /// Sets the executor service used by this build to create futures.
107    ///
108    /// @param executor the executor
109    ///
110    public void executor(ExecutorService executor) {
111        this.executor = executor;
112    }
113
114    /// Executing future streams.
115    ///
116    /// @return the awaitable counter
117    ///
118    public AwaitableCounter executingFutureStreams() {
119        return executingFutureStreams;
120    }
121
122    /// Returns the build root.
123    ///
124    /// @return the path
125    ///
126    public Path buildRoot() {
127        return buildRoot;
128    }
129
130    @Override
131    public CommandLine commandLine() {
132        return commandLine;
133    }
134
135    @Override
136    public String property(String name, String defaultValue) {
137        return jdbldProperties.getProperty(name,
138            defaultValue);
139    }
140
141    /// Returns the context.
142    ///
143    /// @return the optional
144    ///
145    public static Optional<DefaultBuildContext> context() {
146        if (scopedBuildContext.isBound()) {
147            return Optional.of(scopedBuildContext.get());
148        }
149        return Optional.empty();
150    }
151
152    /// Return a carrier with this context available from [#context].
153    ///
154    /// @return the carrier
155    ///
156    public ScopedValue.Carrier inScope() {
157        return ScopedValue.where(scopedBuildContext, this);
158    }
159
160    /* default */ ContextScoped contextScoped() {
161        var result = new ContextScoped();
162        result.callChainEnd
163            = callChainEnd.isBound() ? callChainEnd.get() : null;
164        return result;
165    }
166
167    /// Return a carrier with this context available from [#context] and
168    /// the provider invocation allowed flag set.
169    ///
170    /// @param carrier the carrier
171    /// @return the augmented carrier
172    ///
173    /* default */ ScopedValue.Carrier
174            inScopeForProviderCall(ContextScoped carrier) {
175        return inScope()
176            .where(callChainEnd, carrier.callChainEnd)
177            .where(providerInvocationAllowed, new AtomicBoolean(true));
178    }
179
180    /* default */ SplitConsole console() {
181        return console;
182    }
183
184    @Override
185    public StatusLine statusLine() {
186        return FutureStream.statusLine.orElse(SplitConsole.nullStatusLine());
187    }
188
189    @Override
190    public PrintStream out() {
191        return console().out();
192    }
193
194    @Override
195    public PrintStream error() {
196        return console().err();
197    }
198
199    @Override
200    public <T extends Resource> Stream<T> resources(ResourceProvider provider,
201            ResourceRequest<T> requested) {
202        return ScopedValue.where(scopedBuildContext, this)
203            .where(providerInvocationAllowed, new AtomicBoolean(true))
204            .where(callChainEnd, new CallChainLink(
205                callChainEnd.isBound() ? callChainEnd.get() : null,
206                new ProviderInvocation<>(provider, requested)))
207            .call(() -> inResourcesContext(provider, requested));
208    }
209
210    @SuppressWarnings({ "PMD.AvoidSynchronizedStatement",
211        "PMD.AvoidInstantiatingObjectsInLoops" })
212    private <T extends Resource> Stream<T> inResourcesContext(
213            ResourceProvider provider, ResourceRequest<T> requested) {
214        var thisInvocation = callChainEnd.get().invocation();
215        var cur = callChainEnd.get().previous;
216        while (cur != null) {
217            if (thisInvocation.equals(cur.invocation())) {
218                throw new BuildException().message("Loop");
219            }
220            cur = cur.previous;
221        }
222        callChains.put(callChainEnd.get().invocation, callChainEnd.get());
223        if (provider instanceof Project project) {
224            var defReq = (DefaultResourceRequest<T>) requested;
225            if (Arrays.asList(defReq.queried()).contains(provider)) {
226                return Stream.empty();
227            }
228            // Log invocation with request
229            var req = defReq.queried(project);
230            // As a project's provide only delegates to other providers
231            // it is inefficient to invoke it asynchronously. Besides, it
232            // leads to recursive invocations of the project's deploy
233            // method too easily and results in a loop detection without
234            // there really being a loop.
235            return ((AbstractProvider) provider).toSpi().provide(req);
236        }
237        var req = requested;
238        if (!req.uses().isEmpty()) {
239            req = requested.using(EnumSet.noneOf(Intent.class));
240        }
241        if (!requested.type().equals(CleanlinessType)) {
242            return cache
243                .computeIfAbsent(new ProviderInvocation<>(provider, req),
244                    k -> new FutureStream<T>(this, k.provider(), k.request()))
245                .stream();
246        }
247
248        // Special handling for cleanliness. Clean one by one...
249        synchronized (executor) {
250            // Await completion of all generating threads
251            try {
252                executingFutureStreams().await(0);
253            } catch (InterruptedException e) {
254                throw new BuildException().cause(e);
255            }
256        }
257        var result = ((AbstractProvider) provider).toSpi().provide(requested);
258        // Purge cached results from provider
259        cache.purge(provider);
260        return result;
261    }
262
263    /* default */ static ScopedValue<CallChainLink> callChainEnd() {
264        return callChainEnd;
265    }
266
267    /* default */ List<ProviderInvocation<?>>
268            callChain(ProviderInvocation<?> running) {
269        var cur = callChains.get(running);
270        List<ProviderInvocation<?>> result = new LinkedList<>();
271        while (cur != null) {
272            result.add(cur.invocation);
273            cur = cur.previous;
274        }
275        return result;
276    }
277
278    /// Checks if is provider invocation is allowed. Clears the
279    /// allowed flag to also detect nested invocations.
280    ///
281    /// @return true, if is provider invocation allowed
282    ///
283    public static boolean isProviderInvocationAllowed() {
284        return providerInvocationAllowed.isBound()
285            && providerInvocationAllowed.get().getAndSet(false);
286    }
287
288    @Override
289    public void close() {
290        executor.shutdownNow();
291        console.close();
292    }
293
294    /* default */ Future<AbstractRootProject> buildProject() {
295        return buildProject;
296    }
297
298    /// Creates and initializes the root project and the sub projects.
299    /// Adds the sub projects to the root project automatically. This
300    /// method should be used if the launcher detects the sub projects
301    /// e.g. by reflection and the root project does not add its sub
302    /// projects itself.
303    ///
304    /// @param buildRoot the build root
305    /// @param rootProject the root project
306    /// @param subprojects the sub projects
307    /// @param jdbldProps the builder properties
308    /// @param commandLine the command line
309    /// @return the root project
310    ///
311    public static AbstractRootProject createProjects(
312            Path buildRoot, Class<? extends RootProject> rootProject,
313            List<Class<? extends Project>> subprojects,
314            Properties jdbldProps, CommandLine commandLine) {
315        try {
316            return ScopedValue
317                .where(scopedBuildContext,
318                    new DefaultBuildContext(buildRoot, jdbldProps, commandLine))
319                .call(() -> {
320                    var result = (AbstractRootProject) rootProject
321                        .getConstructor().newInstance();
322                    result.unlockProviders();
323                    subprojects.forEach(result::project);
324                    scopedBuildContext.get().buildProject.complete(result);
325                    return result;
326                });
327        } catch (SecurityException | NegativeArraySizeException
328                | IllegalArgumentException | ReflectiveOperationException e) {
329            throw new IllegalArgumentException(e);
330        }
331    }
332}