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