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    /// The common project properties.
171    ///
172    @SuppressWarnings("PMD.FieldNamingConventions")
173    enum Properties implements PropertyKey {
174
175        /// The Build directory. Created artifacts should be put there.
176        /// Defaults to [Path] "build".
177        BuildDirectory(Path.of("build")),
178        
179        /// The Encoding of files in the project.
180        Encoding("UTF-8"),
181        
182        /// The version of the project. Surprisingly, there is no
183        /// agreed upon version type for Java (see e.g. 
184        /// ["Version Comparison in Java"](https://www.baeldung.com/java-comparing-versions)).
185        /// Therefore the version is represented as a string with "0.0.0"
186        /// as default.
187        Version("0.0.0");
188
189        private final Object defaultValue;
190
191        <T> Properties(T defaultValue) {
192            this.defaultValue = defaultValue;
193        }
194
195        @Override
196        @SuppressWarnings("unchecked")
197        public <T> T defaultValue() {
198            return (T)defaultValue;
199        }
200    }
201
202    /// Returns the root project.
203    ///
204    /// @return the project
205    ///
206    RootProject rootProject();
207
208    /// Returns the instance of the given project class. Projects
209    /// are created lazily by the builder and must be accessed
210    /// via this method.
211    ///
212    /// @param project the requested project's type
213    /// @return the project
214    ///
215    Project project(Class<? extends Project> project);
216
217    /// Returns the parent project. The root project has no parent.
218    ///
219    /// @return the parent project
220    ///
221    Optional<Project> parentProject();
222    
223    /// Returns the project's directory.
224    ///
225    /// @return the path
226    ///
227    Path directory();
228
229    /// Returns the directory where the project's [Generator]s should
230    /// create the artifacts. This is short for 
231    /// `directory().resolve((Path) get(Properties.BuildDirectory))`.
232    ///
233    /// @return the path
234    ///
235    default Path buildDirectory() {
236        return directory().resolve((Path) get(Properties.BuildDirectory));
237    }
238
239    /// Adds a provider to the project that generates resources which
240    /// are then provided by the project. For "normal" projects, the
241    /// generated resources are assumed to be provided to dependents of
242    /// the project, so the invocation is shorthand for  
243    /// `dependency(Intent.Supply, generator)`.
244    ///
245    /// For projects that implement [MergedTestProject], generated resources
246    /// are usually intended to be used by the project itself only, so
247    /// the invocation is short for `dependency(Intent.Consume, generator)`.
248    ///
249    /// @param generator the provider
250    /// @return the project
251    ///
252    Project generator(Generator generator);
253
254    /// Uses the supplier to create a provider, passing this project as 
255    /// argument and adds the result as a generator to this project. This
256    /// is a convenience method to add a provider to the project by writing
257    /// (in a project's constructor):
258    /// 
259    /// ```java
260    /// generator(Provider::new);
261    /// ```
262    /// instead of:
263    /// 
264    /// ```java
265    /// generator(new Provider(this));
266    /// ```
267    ///
268    /// @param <T> the generic type
269    /// @param supplier the supplier
270    /// @return the project for method chaining
271    ///
272    default <T extends Generator> T generator(Function<Project, T> supplier) {
273        var provider = supplier.apply(this);
274        generator(provider);
275        return provider;
276    }
277
278    /// Adds a provider that contributes resources to the project with
279    /// the given intended usage.
280    ///
281    /// While this could be used to add a [Generator] to the project
282    /// as a provider with [Intent#Supply], it is recommended to use
283    /// one of the "generator" methods for better readability.
284    ///
285    /// @param intent the dependency type
286    /// @param provider the provider
287    /// @return the project for method chaining
288    /// @see generator(Generator)
289    /// @see generator(Function)
290    ///
291    Project dependency(Intent intent, ResourceProvider provider);
292
293    /// Uses the supplier to create a provider, passing this project as 
294    /// argument and adds the result as a dependency to this project. This
295    /// is a convenience method to add a provider to the project by writing
296    /// (in a project's constructor):
297    /// 
298    /// ```java
299    /// dependency(intent, Provider::new);
300    /// ```
301    /// instead of:
302    /// 
303    /// ```java
304    /// dependency(intent, new Provider(this));
305    /// ```
306    ///
307    /// @param <T> the generic type
308    /// @param intent the intent
309    /// @param supplier the supplier
310    /// @return the project for method chaining
311    ///
312    default <T extends ResourceProvider> T dependency(Intent intent,
313            Function<Project, T> supplier) {
314        var provider = supplier.apply(this);
315        dependency(intent, provider);
316        return provider;
317    }
318    
319    /// Return a provider selection without any restrictions.
320    ///
321    /// @return the provider selection
322    ///
323    ProviderSelection providers();
324    
325    /// Return a provider selection that is restricted to the given intents.
326    ///
327    /// @param intents the intents
328    /// @return the provider selection
329    ///
330    ProviderSelection providers(Set<Intent> intents);
331    
332    /// Return a provider selection that is restricted to the given intents.
333    ///
334    /// @param intent the intent
335    /// @param intents the intents
336    /// @return the provider selection
337    ///
338    default ProviderSelection providers(Intent intent, Intent... intents) {
339        return providers(EnumSet.of(intent, intents));
340    }
341
342    /// Short for `directory().relativize(other)`.
343    ///
344    /// @param other the other path
345    /// @return the relativized path
346    ///
347    default Path relativize(Path other) {
348        return directory().relativize(other);
349    }
350
351    /// Sets the given property to the given value.
352    /// 
353    /// Regrettably, there is no way to enforce at compile time that the
354    /// type of the value passed to `set` matches the type of the property.
355    /// An implementation must check this at runtime by verifying that the
356    /// given value is assignable to the default value. 
357    ///
358    /// @param property the property
359    /// @param value the value
360    /// @return the project
361    ///
362    Project set(PropertyKey property, Object value);
363    
364    /// Returns value of the given property of the project. If the
365    /// property is not set, the parent project's value is returned.
366    /// If neither is set, the property's default value is returned.
367    ///
368    /// @param <T> the property type
369    /// @param property the property
370    /// @return the property
371    ///
372    <T> T get(PropertyKey property);
373   
374    /// Convenience method to create a new resource. Short for invoking
375    /// [ResourceFactory#create] with the given type as first argument,
376    /// the current project as second argument and the remaining given
377    /// arguments appended.
378    ///
379    /// @param <T> the resource type
380    /// @param type the type
381    /// @param args the arguments
382    /// @return the resource
383    ///
384    <T extends Resource> T newResource(ResourceType<T> type,
385            Object... args);
386
387    /// Convenience method for reading the content of a file into a
388    /// [String]. The path is resolved against the project's directory.
389    ///
390    /// @param path the path
391    /// @return the string
392    ///
393    default String readString(Path path) {
394        try {
395            return Files.readString(directory().resolve(path));
396        } catch (IOException e) {
397            throw new BuildException("Cannot read file: %s", e)
398                .from(this).cause(e);
399        }
400    }
401
402    
403}