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}