001/* 002 * JDrupes Builder 003 * Copyright (C) 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 java.util.Arrays; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.Map; 025import java.util.Objects; 026import java.util.concurrent.ExecutionException; 027import java.util.concurrent.Future; 028import java.util.stream.Collectors; 029import org.jdrupes.builder.api.BuildException; 030import org.jdrupes.builder.api.ConfigurationException; 031import static org.jdrupes.builder.api.Intent.*; 032import org.jdrupes.builder.api.NamedParameter; 033import org.jdrupes.builder.api.Project; 034import org.jdrupes.builder.api.ResourceRequest; 035import static org.jdrupes.builder.api.ResourceType.*; 036import org.jdrupes.builder.api.RootProject; 037 038/// A base class for implementing [RootProject]s. 039/// 040public abstract class AbstractRootProject extends AbstractProject 041 implements RootProject { 042 043 /* default */ @SuppressWarnings("PMD.FieldNamingConventions") 044 static final ScopedValue< 045 AbstractRootProject> scopedRootProject = ScopedValue.newInstance(); 046 private DefaultBuildContext context; 047 private final Map<String, CommandData> commands; 048 private final Map<Class<? extends Project>, Future<Project>> projects; 049 050 /// Initializes a new abstract root project. 051 /// 052 /// @param params the params 053 /// 054 @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") 055 public AbstractRootProject(NamedParameter<?>... params) { 056 super(params); 057 context = LauncherBase.context(); 058 059 // ConcurrentHashMap does not support null values. 060 projects = Collections.synchronizedMap(new HashMap<>()); 061 commands = new HashMap<>(); 062 commandAlias("clean").description("Removes generated resources") 063 .projects("**") 064 .resources(of(CleanlinessType).using(Supply, Consume)); 065 } 066 067 @Override 068 public DefaultBuildContext context() { 069 // In case super() calls context() before we set it 070 if (context == null) { 071 context = LauncherBase.context(); 072 } 073 return context; 074 } 075 076 /// Close. 077 /// 078 @Override 079 public void close() { 080 if (this instanceof RootProject) { 081 context().close(); 082 } 083 } 084 085 /// Root project. 086 /// 087 /// @return the root project 088 /// 089 @Override 090 public RootProject rootProject() { 091 return this; 092 } 093 094 /// Project. 095 /// 096 /// @param prjCls the prj cls 097 /// @return the project 098 /// 099 @Override 100 public Project project(Class<? extends Project> prjCls) { 101 if (this.getClass().equals(prjCls)) { 102 return this; 103 } 104 try { 105 return projects.computeIfAbsent(prjCls, this::futureProject).get(); 106 } catch (InterruptedException | ExecutionException e) { 107 throw new BuildException().from(this).cause(e); 108 } 109 } 110 111 private Future<Project> futureProject(Class<? extends Project> prjCls) { 112 var snapshot = ScopedValueContext.snapshot(); 113 return context().executor().submit(() -> { 114 var origThreadName = Thread.currentThread().getName(); 115 try { 116 Thread.currentThread().setName("Creating " + prjCls); 117 return snapshot.call(() -> createProject(prjCls)); 118 } finally { 119 Thread.currentThread().setName(origThreadName); 120 } 121 }); 122 } 123 124 private Project createProject(Class<? extends Project> prjCls) { 125 try { 126 return ScopedValue.where(scopedRootProject, this) 127 .call(() -> { 128 var result = prjCls.getConstructor().newInstance(); 129 ((AbstractProject) result).unlockProviders(); 130 return result; 131 }); 132 } catch (SecurityException | ReflectiveOperationException e) { 133 throw new IllegalArgumentException(e); 134 } 135 } 136 137 /// Command alias. 138 /// 139 /// @param name the name 140 /// @return the root project. command builder 141 /// 142 @Override 143 public RootProject.CommandBuilder commandAlias(String name) { 144 if (!(this instanceof RootProject)) { 145 throw new ConfigurationException().from(this).message( 146 "Commands can only be defined for the root project."); 147 } 148 return new CommandBuilder((RootProject) this, name); 149 } 150 151 /// The Class CommandBuilder. 152 /// 153 public class CommandBuilder implements RootProject.CommandBuilder { 154 private final RootProject rootProject; 155 private final String name; 156 private String description; 157 private String[] projects = { "" }; 158 private String[] without = {}; 159 160 /// Initializes a new command builder. 161 /// 162 /// @param rootProject the root project 163 /// @param name the name 164 /// 165 public CommandBuilder(RootProject rootProject, String name) { 166 this.rootProject = rootProject; 167 this.name = name; 168 } 169 170 @Override 171 public CommandBuilder description(String description) { 172 this.description = Objects.requireNonNull(description); 173 return this; 174 } 175 176 @Override 177 @SuppressWarnings("PMD.ArrayIsStoredDirectly") 178 public RootProject.CommandBuilder projects(String... projects) { 179 this.projects = projects; 180 return this; 181 } 182 183 @Override 184 @SuppressWarnings("PMD.ArrayIsStoredDirectly") 185 public RootProject.CommandBuilder without(String... without) { 186 this.without = without; 187 return this; 188 } 189 190 @Override 191 public RootProject resources(ResourceRequest<?>... requests) { 192 for (int i = 0; i < requests.length; i++) { 193 if (requests[i].uses().isEmpty()) { 194 requests[i] = requests[i].usingAll(); 195 } 196 } 197 if (description == null) { 198 description = "Generate resources of type " 199 + Arrays.stream(requests).map(r -> r.type().toString()) 200 .collect(Collectors.joining(", ")); 201 } 202 commands.put(name, new CommandData(projects, without, requests, 203 description)); 204 return rootProject; 205 } 206 } 207 208 /// The Record CommandData. 209 /// 210 /// @param patterns the patterns 211 /// @param without the without 212 /// @param requests the requests 213 /// @param description the description 214 /// 215 public record CommandData(String[] patterns, String[] without, 216 ResourceRequest<?>[] requests, String description) { 217 } 218 219 /// Lookup command. 220 /// 221 /// @param name the name 222 /// @return the command data 223 /// 224 public CommandData lookupCommand(String name) { 225 return commands.getOrDefault(name, 226 new CommandData(new String[] { "" }, new String[0], 227 new ResourceRequest[0], null)); 228 } 229 230 /// Return the registered command names with their descriptions. 231 /// The values in the map (the descriptions) are never null, but 232 /// may be empty strings. 233 /// 234 /// @return the command names with their descriptions 235 /// 236 public Map<String, String> commands() { 237 return commands.entrySet().stream().collect(Collectors.toMap( 238 Map.Entry::getKey, e -> e.getValue().description() != null 239 ? e.getValue().description() 240 : "")); 241 } 242 243}