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 /// 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 project's name and its directory in parentheses. 230 /// Appending the directory is omitted if it is the same as the name. 231 /// 232 /// @return the string 233 /// 234 default String nameWithDirectory() { 235 StringBuilder result = new StringBuilder(name()); 236 if (directory() != null) { 237 var relDir = rootProject().directory().relativize(directory()); 238 if (!relDir.toString().equals(name()) 239 && !relDir.toString().isEmpty()) { 240 result.append(" (in ").append(relDir).append(')'); 241 } 242 } 243 return result.toString(); 244 245 } 246 247 /// Returns the directory where the project's [Generator]s should 248 /// create the artifacts. This is short for 249 /// `directory().resolve((Path) get(Properties.BuildDirectory))`. 250 /// 251 /// @return the path 252 /// 253 default Path buildDirectory() { 254 return directory().resolve((Path) get(Properties.BuildDirectory)); 255 } 256 257 /// Adds a provider to the project that generates resources which 258 /// are then provided by the project. For "normal" projects, the 259 /// generated resources are assumed to be provided to dependents of 260 /// the project, so the invocation is shorthand for 261 /// `dependency(Intent.Supply, generator)`. 262 /// 263 /// For projects that implement [MergedTestProject], generated resources 264 /// are usually intended to be used by the project itself only, so 265 /// the invocation is short for `dependency(Intent.Consume, generator)`. 266 /// 267 /// @param generator the provider 268 /// @return the project 269 /// 270 Project generator(Generator generator); 271 272 /// Uses the supplier to create a provider, passing this project as 273 /// argument and adds the result as a generator to this project. This 274 /// is a convenience method to add a provider to the project by writing 275 /// (in a project's constructor): 276 /// 277 /// ```java 278 /// generator(Provider::new); 279 /// ``` 280 /// instead of: 281 /// 282 /// ```java 283 /// generator(new Provider(this)); 284 /// ``` 285 /// 286 /// @param <T> the generic type 287 /// @param supplier the supplier 288 /// @return the project for method chaining 289 /// 290 default <T extends Generator> T generator(Function<Project, T> supplier) { 291 var provider = supplier.apply(this); 292 generator(provider); 293 return provider; 294 } 295 296 /// Adds a provider that contributes resources to the project with 297 /// the given intended usage. 298 /// 299 /// While this could be used to add a [Generator] to the project 300 /// as a provider with [Intent#Supply], it is recommended to use 301 /// one of the "generator" methods for better readability. 302 /// 303 /// @param intent the dependency type 304 /// @param provider the provider 305 /// @return the project for method chaining 306 /// @see generator(Generator) 307 /// @see generator(Function) 308 /// 309 Project dependency(Intent intent, ResourceProvider provider); 310 311 /// Uses the supplier to create a provider, passing this project as 312 /// argument and adds the result as a dependency to this project. This 313 /// is a convenience method to add a provider to the project by writing 314 /// (in a project's constructor): 315 /// 316 /// ```java 317 /// dependency(intent, Provider::new); 318 /// ``` 319 /// instead of: 320 /// 321 /// ```java 322 /// dependency(intent, new Provider(this)); 323 /// ``` 324 /// 325 /// @param <T> the generic type 326 /// @param intent the intent 327 /// @param supplier the supplier 328 /// @return the project for method chaining 329 /// 330 default <T extends ResourceProvider> T dependency(Intent intent, 331 Function<Project, T> supplier) { 332 var provider = supplier.apply(this); 333 dependency(intent, provider); 334 return provider; 335 } 336 337 /// Return a provider selection without any restrictions. 338 /// 339 /// @return the provider selection 340 /// 341 ProviderSelection providers(); 342 343 /// Return a provider selection that is restricted to the given intents. 344 /// 345 /// @param intents the intents 346 /// @return the provider selection 347 /// 348 ProviderSelection providers(Set<Intent> intents); 349 350 /// Return a provider selection that is restricted to the given intents. 351 /// 352 /// @param intent the intent 353 /// @param intents the intents 354 /// @return the provider selection 355 /// 356 default ProviderSelection providers(Intent intent, Intent... intents) { 357 return providers(EnumSet.of(intent, intents)); 358 } 359 360 /// Short for `directory().relativize(other)`. 361 /// 362 /// @param other the other path 363 /// @return the relativized path 364 /// 365 default Path relativize(Path other) { 366 return directory().relativize(other); 367 } 368 369 /// Sets the given property to the given value. 370 /// 371 /// Regrettably, there is no way to enforce at compile time that the 372 /// type of the value passed to `set` matches the type of the property. 373 /// An implementation must check this at runtime by verifying that the 374 /// given value is assignable to the default value. 375 /// 376 /// @param property the property 377 /// @param value the value 378 /// @return the project 379 /// 380 Project set(PropertyKey property, Object value); 381 382 /// Returns value of the given property of the project. If the 383 /// property is not set, the parent project's value is returned. 384 /// If neither is set, the property's default value is returned. 385 /// 386 /// @param <T> the property type 387 /// @param property the property 388 /// @return the property 389 /// 390 <T> T get(PropertyKey property); 391 392 /// Convenience method for reading the content of a file into a 393 /// [String]. The path is resolved against the project's directory. 394 /// 395 /// @param path the path 396 /// @return the string 397 /// 398 default String readString(Path path) { 399 try { 400 return Files.readString(directory().resolve(path)); 401 } catch (IOException e) { 402 throw new BuildException().from(this).cause(e); 403 } 404 } 405 406 407}