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.Cleanliness; 028import org.jdrupes.builder.api.ConfigurationException; 029import static org.jdrupes.builder.api.Intent.*; 030import org.jdrupes.builder.api.NamedParameter; 031import org.jdrupes.builder.api.Project; 032import org.jdrupes.builder.api.ResourceRequest; 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 Map<String, CommandData> commands; 044 private final Map<Class<? extends Project>, Future<Project>> projects; 045 046 /// Initializes a new abstract root project. 047 /// 048 /// @param params the params 049 /// 050 @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") 051 public AbstractRootProject(NamedParameter<?>... params) { 052 super(params); 053 // ConcurrentHashMap does not support null values. 054 projects = Collections.synchronizedMap(new HashMap<>()); 055 commands = new HashMap<>(); 056 commandAlias("clean").projects("**") 057 .resources(of(Cleanliness.class).using(Supply, Consume)); 058 } 059 060 @Override 061 public void close() { 062 if (this instanceof RootProject) { 063 context().close(); 064 } 065 } 066 067 @Override 068 public RootProject rootProject() { 069 return this; 070 } 071 072 @Override 073 public Project project(Class<? extends Project> prjCls) { 074 if (this.getClass().equals(prjCls)) { 075 return this; 076 } 077 try { 078 return projects.computeIfAbsent(prjCls, this::futureProject) 079 .get(); 080 } catch (InterruptedException | ExecutionException e) { 081 throw new BuildException().from(this).cause(e); 082 } 083 } 084 085 private Future<Project> futureProject(Class<? extends Project> prjCls) { 086 @SuppressWarnings("PMD.CloseResource") 087 final DefaultBuildContext context = context(); 088 return context().executor().submit(() -> { 089 var origThreadName = Thread.currentThread().getName(); 090 try { 091 Thread.currentThread().setName("Creating " + prjCls); 092 return context.call(() -> createProject(prjCls)); 093 } finally { 094 Thread.currentThread().setName(origThreadName); 095 } 096 }); 097 } 098 099 private Project createProject(Class<? extends Project> prjCls) { 100 try { 101 return ScopedValue.where(scopedRootProject, this) 102 .call(() -> { 103 var result = prjCls.getConstructor().newInstance(); 104 ((AbstractProject) result).unlockProviders(); 105 return result; 106 }); 107 } catch (SecurityException | ReflectiveOperationException e) { 108 throw new IllegalArgumentException(e); 109 } 110 } 111 112 @Override 113 public RootProject.CommandBuilder commandAlias(String name) { 114 if (!(this instanceof RootProject)) { 115 throw new ConfigurationException().from(this).message( 116 "Commands can only be defined for the root project."); 117 } 118 return new CommandBuilder((RootProject) this, name); 119 } 120 121 /// The Class CommandBuilder. 122 /// 123 public class CommandBuilder implements RootProject.CommandBuilder { 124 private final RootProject rootProject; 125 private final String name; 126 private String projects = ""; 127 128 /// Initializes a new command builder. 129 /// 130 /// @param rootProject the root project 131 /// @param name the name 132 /// 133 public CommandBuilder(RootProject rootProject, String name) { 134 this.rootProject = rootProject; 135 this.name = name; 136 } 137 138 /// Projects. 139 /// 140 /// @param projects the projects 141 /// @return the root project. command builder 142 /// 143 @Override 144 public RootProject.CommandBuilder projects(String projects) { 145 this.projects = projects; 146 return this; 147 } 148 149 /// Resources. 150 /// 151 /// @param requests the requests 152 /// @return the root project 153 /// 154 @Override 155 public RootProject resources(ResourceRequest<?>... requests) { 156 for (int i = 0; i < requests.length; i++) { 157 if (requests[i].uses().isEmpty()) { 158 requests[i] = requests[i].usingAll(); 159 } 160 } 161 commands.put(name, new CommandData(projects, requests)); 162 return rootProject; 163 } 164 } 165 166 /// The Record CommandData. 167 /// 168 /// @param pattern the pattern 169 /// @param requests the requests 170 /// 171 public record CommandData(String pattern, ResourceRequest<?>... requests) { 172 } 173 174 /// Lookup command. 175 /// 176 /// @param name the name 177 /// @return the command data 178 /// 179 public CommandData lookupCommand(String name) { 180 return commands.getOrDefault(name, 181 new CommandData("", new ResourceRequest[0])); 182 } 183 184}