001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 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.api;
020
021import java.io.IOException;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.EnumSet;
025import java.util.Optional;
026import java.util.Set;
027import java.util.function.Function;
028
029/// [Project]s are used to structure the build configuration. Every build
030/// configuration has a single root project and may contain sub-projects.
031/// The root project serves as the entry point for the build. Resources
032/// provided by the builder are typically provided by the root project,
033/// which acts as the central access point of the build configuration.
034///
035/// Projects are [ResourceProvider]s that obtain resources from related
036/// [ResourceProvider]s. Conceptually, a project acts as a router for
037/// requests and resources, with its behavior depending on the intended
038/// usage of the resources obtained from the providers registered as
039/// dependencies. The intended usage is indicated by the [Intent] that
040/// attributes the relationship between a project and its related
041/// resource providers.
042///
043/// ## Attributing relationships to providers
044///
045/// ### Intent Supply
046///
047/// ![Intent Supply](supply-demo.svg)
048///
049/// Resources from a provider added with [Intent#Supply] are made available
050/// by the project to entities that depend on it. [Intent#Supply] implies
051/// that the resources are generated specifically for the project,
052/// typically by a [Generator] that belongs to the project.
053///
054/// ### Intent Consume and Reveal
055///
056/// ![Intent Consume](consume-demo.svg)
057///
058/// Resources from a provider added with [Intent#Consume] or
059/// [Intent#Reveal] are typically used only by a project's generators.
060/// If a provider is added with [Intent#Reveal], its resources are also
061/// provided by the project when they are explicitly included in a
062/// request.
063///
064/// ### Intent Expose
065///
066/// ![Intent Expose](expose-demo.svg)
067///
068/// Resources from a provider added with [Intent#Expose] (typically
069/// another project) are used by a project's generators and are also
070/// provided by the project to entities that depend on it.
071///
072/// ### Intent Forward
073///
074/// ![Intent Forward](forward-demo.svg)
075///
076/// Resources from a provider added with [Intent#Forward] (typically
077/// another project) are provided by the project to entities that depend
078/// on it. These resources are not intended to be used by the project's
079/// generators. This cannot be enforced, however, as generators may still
080/// access them via [Project#providers()].
081///
082/// ## Behavior as resource provider
083///
084/// In its role as a [ResourceProvider], a [Project]
085/// [provides resources][ResourceProvider#resources] obtained from its
086/// dependencies. A [ResourceRequest] controls to which dependencies a
087/// request is forwarded by including the respective [Intent]s in the
088/// set returned by [ResourceRequest#uses].
089///
090/// Concepts from other build tools, such as Gradle’s dependency
091/// configurations, can be mapped to resource requests for classpath
092/// elements using sets of [Intent]s as follows:
093///
094/// <table class="simple-table">
095///   <thead>
096///     <tr>
097///       <th style="white-space: nowrap;">Intent \ Config.</th>
098///       <th>Api</th>
099///       <th>Implementation</th>
100///       <th>Compile only</th>
101///       <th>Runtime only</th>
102///     </tr>
103///   </thead>
104///   <tbody>
105///     <tr><td>Supply</td><td>X</td><td>X</td><td></td><td></td></tr>
106///     <tr><td>Consume</td><td></td><td></td><td>X</td><td></td></tr>
107///     <tr><td>Reveal</td><td></td><td>X</td><td></td><td></td></tr>
108///     <tr><td>Expose</td><td>X</td><td>X</td><td></td><td></td></tr>
109///     <tr><td>Forward</td><td></td><td></td><td></td><td>X</td></tr>
110///   </tbody>
111/// </table>
112///
113/// To ensure consistent results, a project adjusts a request before
114/// forwarding it to a dependency of type [Project] (typically a
115/// sub-project). If the request uses `Consume` or `Expose`, `Consume`
116/// is removed and `Expose` and `Supply` are added. The reason is that,
117/// regardless of how a sub-project contributes to another project, the
118/// resources it contributes are always those that are part of its API.
119///
120/// To avoid misuse of [Intent]s, all intents are removed from a request
121/// before it is forwarded to a dependency that is not a project.
122///
123/// ## Factory method for resources
124///
125/// As a convenience, this interface also defines a shortcut for creating
126/// [Resource]s.
127///
128/// @startuml supply-demo.svg
129/// object "project: Project" as project
130/// object "dependant" as dependant
131/// dependant -right-> project
132/// object "generator: Generator" as generator
133/// project *-down-> generator: "<<Supply>>"
134/// @enduml
135///
136/// @startuml expose-demo.svg
137/// object "project: Project" as project
138/// object "dependant" as dependant
139/// dependant -right-> project
140/// object "providing: Project" as providing
141/// project *-right-> providing: "<<Expose>>"
142/// object "generator: Generator" as generator
143/// project *-down-> generator: "   "
144/// generator .up.> project: "provided"
145/// @enduml
146///
147/// @startuml consume-demo.svg
148/// object "project: Project" as project
149/// object "dependant" as dependant
150/// dependant -right-> project
151/// object "providing: Project" as providing
152/// project *-right-> providing: "<<Consume>>"
153/// object "generator: Generator" as generator
154/// project *-down-> generator: "   "
155/// generator .up.> project: "provided"
156/// @enduml
157///
158/// @startuml forward-demo.svg
159/// object "project: Project" as project
160/// object "dependant" as dependant
161/// dependant -right-> project
162/// object "providing: Project" as providing
163/// project *-right-> providing: "<<Forward>>"
164/// object "generator: Generator" as generator
165/// project *-down-> generator
166/// @enduml
167///
168public interface Project extends ResourceProvider {
169
170    /// General project properties.
171    ///
172    @SuppressWarnings("PMD.FieldNamingConventions")
173    final class Properties {
174
175        /// The Build directory. Created artifacts should be put there.
176        /// Defaults to [Path] "build".
177        public static final PropertyKey<Path> BuildDirectory
178            = new PropertyKey<>(Path.of("build"));
179
180        /// The Encoding of files in the project.
181        public static final PropertyKey<String> Encoding
182            = new PropertyKey<>("UTF-8");
183
184        /// The version of the project. Surprisingly, there is no
185        /// agreed upon version type for Java (see e.g. 
186        /// ["Version Comparison in Java"](https://www.baeldung.com/java-comparing-versions)).
187        /// Therefore the version is represented as a string with "0.0.0"
188        /// as default.
189        public static final PropertyKey<String> Version
190            = new PropertyKey<>("0.0.0");
191
192        private Properties() {
193            // Make javadoc happy.
194        }
195    }
196
197    /// Returns the root project.
198    ///
199    /// @return the project
200    ///
201    RootProject rootProject();
202
203    /// Returns the instance of the given project class. Projects
204    /// are created lazily by the builder and must be accessed
205    /// via this method.
206    ///
207    /// @param project the requested project's type
208    /// @return the project
209    ///
210    Project project(Class<? extends Project> project);
211
212    /// Returns the parent project. The root project has no parent.
213    ///
214    /// @return the parent project
215    ///
216    Optional<Project> parentProject();
217
218    /// Returns the project's directory.
219    ///
220    /// @return the path
221    ///
222    Path directory();
223
224    /// Returns the project's name and its directory appended in parentheses.
225    /// Appending the directory is omitted if it's name equals the
226    /// project's name.
227    ///
228    /// @return the string
229    ///
230    default String nameWithDirectory() {
231        StringBuilder result = new StringBuilder(name());
232        if (directory() != null) {
233            var relDir = rootProject().directory().relativize(directory());
234            if (!relDir.toString().equals(name())
235                && !relDir.toString().isEmpty()) {
236                result.append(" (in ").append(relDir).append(')');
237            }
238        }
239        return result.toString();
240
241    }
242
243    /// Returns the directory where the project's [Generator]s should
244    /// create the artifacts. This is short for 
245    /// `directory().resolve((Path) get(Project.BuildDirectory))`.
246    ///
247    /// @return the path
248    ///
249    default Path buildDirectory() {
250        return directory().resolve(get(Properties.BuildDirectory));
251    }
252
253    /// Adds a provider to the project that generates resources which
254    /// are then provided by the project. For "normal" projects, the
255    /// generated resources are assumed to be provided to dependents of
256    /// the project, so the invocation is shorthand for  
257    /// `dependency(Intent.Supply, generator)`.
258    ///
259    /// For projects that implement [MergedTestProject], generated resources
260    /// are usually intended to be used by the project itself only, so
261    /// the invocation is short for `dependency(Intent.Consume, generator)`.
262    ///
263    /// @param generator the provider
264    /// @return the project
265    ///
266    Project generator(Generator generator);
267
268    /// Uses the supplier to create a provider, passing this project as 
269    /// argument and adds the result as a generator to this project. This
270    /// is a convenience method to add a provider to the project by writing
271    /// (in a project's constructor):
272    /// 
273    /// ```java
274    /// generator(Provider::new);
275    /// ```
276    /// instead of:
277    /// 
278    /// ```java
279    /// generator(new Provider(this));
280    /// ```
281    ///
282    /// @param <T> the generic type
283    /// @param supplier the supplier
284    /// @return the project for method chaining
285    ///
286    default <T extends Generator> T generator(Function<Project, T> supplier) {
287        var provider = supplier.apply(this);
288        generator(provider);
289        return provider;
290    }
291
292    /// Adds a provider that contributes resources to the project with
293    /// the given intended usage.
294    ///
295    /// While this could be used to add a [Generator] to the project
296    /// as a provider with [Intent#Supply], it is recommended to use
297    /// one of the "generator" methods for better readability.
298    ///
299    /// @param intent the dependency type
300    /// @param provider the provider
301    /// @return the project for method chaining
302    /// @see generator(Generator)
303    /// @see generator(Function)
304    ///
305    Project dependency(Intent intent, ResourceProvider provider);
306
307    /// Uses the supplier to create a provider, passing this project as 
308    /// argument and adds the result as a dependency to this project. This
309    /// is a convenience method to add a provider to the project by writing
310    /// (in a project's constructor):
311    /// 
312    /// ```java
313    /// dependency(intent, Provider::new);
314    /// ```
315    /// instead of:
316    /// 
317    /// ```java
318    /// dependency(intent, new Provider(this));
319    /// ```
320    ///
321    /// @param <T> the generic type
322    /// @param intent the intent
323    /// @param supplier the supplier
324    /// @return the project for method chaining
325    ///
326    default <T extends ResourceProvider> T dependency(Intent intent,
327            Function<Project, T> supplier) {
328        var provider = supplier.apply(this);
329        dependency(intent, provider);
330        return provider;
331    }
332
333    /// Return a provider selection without any restrictions.
334    ///
335    /// @return the provider selection
336    ///
337    ProviderSelection providers();
338
339    /// Return a provider selection that is restricted to the given intents.
340    ///
341    /// @param intents the intents
342    /// @return the provider selection
343    ///
344    ProviderSelection providers(Set<Intent> intents);
345
346    /// Return a provider selection that is restricted to the given intents.
347    ///
348    /// @param intent the intent
349    /// @param intents the intents
350    /// @return the provider selection
351    ///
352    default ProviderSelection providers(Intent intent, Intent... intents) {
353        return providers(EnumSet.of(intent, intents));
354    }
355
356    /// Short for `directory().relativize(other)`.
357    ///
358    /// @param other the other path
359    /// @return the relativized path
360    ///
361    default Path relativize(Path other) {
362        return directory().relativize(other);
363    }
364
365    /// Sets the given property to the given value.
366    /// 
367    /// Note that there is no way to enforce at compile time that the
368    /// type of the value passed to `set` matches the type of the property.
369    /// An implementation must check this at runtime by verifying that the
370    /// given value is assignable to the default value. 
371    ///
372    /// @param <T> the generic type
373    /// @param property the property
374    /// @param value the value
375    /// @return the project
376    ///
377    <T> Project set(PropertyKey<T> property, T value);
378
379    /// Returns value of the given property of the project. If the
380    /// property is not set, the parent project's value is returned.
381    /// If neither is set, the property's default value is returned.
382    ///
383    /// @param <T> the property type
384    /// @param property the property
385    /// @return the property
386    ///
387    <T> T get(PropertyKey<T> property);
388
389    /// Convenience method for reading the content of a file into a
390    /// [String]. The path is resolved against the project's directory.
391    ///
392    /// @param path the path
393    /// @return the string
394    ///
395    default String readString(Path path) {
396        try {
397            return Files.readString(directory().resolve(path));
398        } catch (IOException e) {
399            throw new BuildException().from(this).cause(e);
400        }
401    }
402
403}