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