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/// The Class AbstractRootProject. 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 final 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 context = LauncherBase.context(); 054 super(params); 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 return context; 066 } 067 068 /// Close. 069 /// 070 @Override 071 public void close() { 072 if (this instanceof RootProject) { 073 context().close(); 074 } 075 } 076 077 /// Root project. 078 /// 079 /// @return the root project 080 /// 081 @Override 082 public RootProject rootProject() { 083 return this; 084 } 085 086 /// Project. 087 /// 088 /// @param prjCls the prj cls 089 /// @return the project 090 /// 091 @Override 092 public Project project(Class<? extends Project> prjCls) { 093 if (this.getClass().equals(prjCls)) { 094 return this; 095 } 096 try { 097 return projects.computeIfAbsent(prjCls, this::futureProject).get(); 098 } catch (InterruptedException | ExecutionException e) { 099 throw new BuildException().from(this).cause(e); 100 } 101 } 102 103 private Future<Project> futureProject(Class<? extends Project> prjCls) { 104 var snapshot = ScopedValueContext.snapshot(); 105 return context().executor().submit(() -> { 106 var origThreadName = Thread.currentThread().getName(); 107 try { 108 Thread.currentThread().setName("Creating " + prjCls); 109 return snapshot.call(() -> createProject(prjCls)); 110 } finally { 111 Thread.currentThread().setName(origThreadName); 112 } 113 }); 114 } 115 116 private Project createProject(Class<? extends Project> prjCls) { 117 try { 118 return ScopedValue.where(scopedRootProject, this) 119 .call(() -> { 120 var result = prjCls.getConstructor().newInstance(); 121 ((AbstractProject) result).unlockProviders(); 122 return result; 123 }); 124 } catch (SecurityException | ReflectiveOperationException e) { 125 throw new IllegalArgumentException(e); 126 } 127 } 128 129 /// Command alias. 130 /// 131 /// @param name the name 132 /// @return the root project. command builder 133 /// 134 @Override 135 public RootProject.CommandBuilder commandAlias(String name) { 136 if (!(this instanceof RootProject)) { 137 throw new ConfigurationException().from(this).message( 138 "Commands can only be defined for the root project."); 139 } 140 return new CommandBuilder((RootProject) this, name); 141 } 142 143 /// The Class CommandBuilder. 144 /// 145 public class CommandBuilder implements RootProject.CommandBuilder { 146 private final RootProject rootProject; 147 private final String name; 148 private String[] projects = { "" }; 149 private String[] without = {}; 150 151 /// Initializes a new command builder. 152 /// 153 /// @param rootProject the root project 154 /// @param name the name 155 /// 156 public CommandBuilder(RootProject rootProject, String name) { 157 this.rootProject = rootProject; 158 this.name = name; 159 } 160 161 @Override 162 @SuppressWarnings("PMD.ArrayIsStoredDirectly") 163 public RootProject.CommandBuilder projects(String... projects) { 164 this.projects = projects; 165 return this; 166 } 167 168 @Override 169 @SuppressWarnings("PMD.ArrayIsStoredDirectly") 170 public RootProject.CommandBuilder without(String... without) { 171 this.without = without; 172 return this; 173 } 174 175 @Override 176 public RootProject resources(ResourceRequest<?>... requests) { 177 for (int i = 0; i < requests.length; i++) { 178 if (requests[i].uses().isEmpty()) { 179 requests[i] = requests[i].usingAll(); 180 } 181 } 182 commands.put(name, new CommandData(projects, without, requests)); 183 return rootProject; 184 } 185 } 186 187 /// The Record CommandData. 188 /// 189 /// @param patterns the patterns 190 /// @param without the without 191 /// @param requests the requests 192 /// 193 public record CommandData(String[] patterns, String[] without, 194 ResourceRequest<?>[] requests) { 195 } 196 197 /// Lookup command. 198 /// 199 /// @param name the name 200 /// @return the command data 201 /// 202 public CommandData lookupCommand(String name) { 203 return commands.getOrDefault(name, 204 new CommandData(new String[] { "" }, new String[0], 205 new ResourceRequest[0])); 206 } 207 208}