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}