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}