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}