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