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///  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///  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///  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///  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}