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}