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.List;
026import java.util.Optional;
027import java.util.Properties;
028import java.util.concurrent.ExecutorService;
029import java.util.concurrent.Executors;
030import java.util.concurrent.atomic.AtomicBoolean;
031import java.util.function.Supplier;
032import java.util.stream.Stream;
033import org.apache.commons.cli.CommandLine;
034import org.jdrupes.builder.api.BuildContext;
035import org.jdrupes.builder.api.BuildException;
036import org.jdrupes.builder.api.Intent;
037import org.jdrupes.builder.api.Project;
038import org.jdrupes.builder.api.Resource;
039import org.jdrupes.builder.api.ResourceProvider;
040import org.jdrupes.builder.api.ResourceRequest;
041import static org.jdrupes.builder.api.ResourceType.CleanlinessType;
042import org.jdrupes.builder.api.RootProject;
043import org.jdrupes.builder.api.StatusLine;
044import org.jdrupes.builder.core.FutureStreamCache.Key;
045import org.jdrupes.builder.core.console.SplitConsole;
046
047/// A context for building.
048///
049public class DefaultBuildContext implements BuildContext {
050
051    @SuppressWarnings("PMD.FieldNamingConventions")
052    private static final ScopedValue<AtomicBoolean> providerInvocationAllowed
053        = ScopedValue.newInstance();
054    private final FutureStreamCache cache;
055    private ExecutorService executor
056        = Executors.newVirtualThreadPerTaskExecutor();
057    private final Path buildRoot;
058    private final Properties jdbldProperties;
059    private final CommandLine commandLine;
060    private final AwaitableCounter executingFutureStreams
061        = new AwaitableCounter();
062    private final SplitConsole console;
063    @SuppressWarnings("PMD.FieldNamingConventions")
064    private static final ScopedValue<
065            DefaultBuildContext> scopedBuildContext = ScopedValue.newInstance();
066
067    /// Instantiates a new default build. By default, the build uses
068    /// a virtual thread per task executor.
069    ///
070    /* default */ DefaultBuildContext(Path buildRoot,
071            Properties jdbldProperties, CommandLine commandLine) {
072        this.buildRoot = buildRoot;
073        this.jdbldProperties = jdbldProperties;
074        this.commandLine = commandLine;
075        cache = new FutureStreamCache();
076        console = SplitConsole.open();
077    }
078
079    /// Returns the executor service used by this build to create futures.
080    ///
081    /// @return the executor service
082    ///
083    public ExecutorService executor() {
084        return executor;
085    }
086
087    /// Sets the executor service used by this build to create futures.
088    ///
089    /// @param executor the executor
090    ///
091    public void executor(ExecutorService executor) {
092        this.executor = executor;
093    }
094
095    /// Executing future streams.
096    ///
097    /// @return the awaitable counter
098    ///
099    public AwaitableCounter executingFutureStreams() {
100        return executingFutureStreams;
101    }
102
103    /// Returns the build root.
104    ///
105    /// @return the path
106    ///
107    public Path buildRoot() {
108        return buildRoot;
109    }
110
111    @Override
112    public CommandLine commandLine() {
113        return commandLine;
114    }
115
116    @Override
117    public String property(String name, String defaultValue) {
118        return jdbldProperties.getProperty(name,
119            defaultValue);
120    }
121
122    /// Returns the context.
123    ///
124    /// @return the optional
125    ///
126    public static Optional<DefaultBuildContext> context() {
127        if (scopedBuildContext.isBound()) {
128            return Optional.of(scopedBuildContext.get());
129        }
130        return Optional.empty();
131    }
132
133    /// Call within this context. 
134    ///
135    /// @param <T> the generic type
136    /// @param supplier the supplier
137    /// @return the t
138    ///
139    public <T> T call(Supplier<T> supplier) {
140        return ScopedValue.where(scopedBuildContext, this).call(supplier::get);
141    }
142
143    /* default */ SplitConsole console() {
144        return console;
145    }
146
147    @Override
148    public StatusLine statusLine() {
149        return FutureStream.statusLine.orElse(SplitConsole.nullStatusLine());
150    }
151
152    @Override
153    public PrintStream out() {
154        return console().out();
155    }
156
157    @Override
158    public PrintStream error() {
159        return console().err();
160    }
161
162    @Override
163    public <T extends Resource> Stream<T> resources(ResourceProvider provider,
164            ResourceRequest<T> requested) {
165        return ScopedValue.where(scopedBuildContext, this)
166            .where(providerInvocationAllowed, new AtomicBoolean(true))
167            .call(() -> inResourcesContext(provider, requested));
168    }
169
170    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
171    private <T extends Resource> Stream<T> inResourcesContext(
172            ResourceProvider provider, ResourceRequest<T> requested) {
173        if (provider instanceof Project project) {
174            var defReq = (DefaultResourceRequest<T>) requested;
175            if (Arrays.asList(defReq.queried()).contains(provider)) {
176                return Stream.empty();
177            }
178            // Log invocation with request
179            var req = defReq.queried(project);
180            // As a project's provide only delegates to other providers
181            // it is inefficient to invoke it asynchronously. Besides, it
182            // leads to recursive invocations of the project's deploy
183            // method too easily and results in a loop detection without
184            // there really being a loop.
185            return ((AbstractProvider) provider).toSpi().provide(req);
186        }
187        var req = requested;
188        if (!req.uses().isEmpty()) {
189            req = requested.using(EnumSet.noneOf(Intent.class));
190        }
191        if (!requested.type().equals(CleanlinessType)) {
192            return cache.computeIfAbsent(new Key<>(provider, req),
193                k -> new FutureStream<T>(this, scopedBuildContext,
194                    providerInvocationAllowed, k.provider(), k.request()))
195                .stream();
196        }
197
198        // Special handling for cleanliness. Clean one by one...
199        synchronized (executor) {
200            // Await completion of all generating threads
201            try {
202                executingFutureStreams().await(0);
203            } catch (InterruptedException e) {
204                throw new BuildException().cause(e);
205            }
206        }
207        var result = ((AbstractProvider) provider).toSpi().provide(requested);
208        // Purge cached results from provider
209        cache.purge(provider);
210        return result;
211    }
212
213    /// Checks if is provider invocation is allowed. Clears the
214    /// allowed flag to also detect nested invocations.
215    ///
216    /// @return true, if is provider invocation allowed
217    ///
218    public static boolean isProviderInvocationAllowed() {
219        return providerInvocationAllowed.isBound()
220            && providerInvocationAllowed.get().getAndSet(false);
221    }
222
223    @Override
224    public void close() {
225        executor.shutdownNow();
226        console.close();
227    }
228
229    /// Creates and initializes the root project and the sub projects.
230    /// Adds the sub projects to the root project automatically. This
231    /// method should be used if the launcher detects the sub projects
232    /// e.g. by reflection and the root project does not add its sub
233    /// projects itself.
234    ///
235    /// @param buildRoot the build root
236    /// @param rootProject the root project
237    /// @param subprojects the sub projects
238    /// @param jdbldProps the builder properties
239    /// @param commandLine the command line
240    /// @return the root project
241    ///
242    public static AbstractRootProject createProjects(
243            Path buildRoot, Class<? extends RootProject> rootProject,
244            List<Class<? extends Project>> subprojects,
245            Properties jdbldProps, CommandLine commandLine) {
246        try {
247            return ScopedValue
248                .where(scopedBuildContext,
249                    new DefaultBuildContext(buildRoot, jdbldProps, commandLine))
250                .call(() -> {
251                    var result = (AbstractRootProject) rootProject
252                        .getConstructor().newInstance();
253                    result.unlockProviders();
254                    subprojects.forEach(result::project);
255                    return result;
256
257                });
258        } catch (SecurityException | NegativeArraySizeException
259                | IllegalArgumentException | ReflectiveOperationException e) {
260            throw new IllegalArgumentException(e);
261        }
262    }
263}