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}