001/* 002 * JDrupes Builder 003 * Copyright (C) 2025 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.mvnrepo; 020 021import com.google.common.flogger.FluentLogger; 022import static com.google.common.flogger.LazyArgs.*; 023import java.io.ByteArrayOutputStream; 024import java.io.IOException; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.util.Arrays; 028import java.util.Optional; 029import java.util.function.Consumer; 030import java.util.function.Supplier; 031import java.util.stream.Stream; 032import org.apache.maven.model.Dependency; 033import org.apache.maven.model.DependencyManagement; 034import org.apache.maven.model.Model; 035import org.apache.maven.model.io.DefaultModelWriter; 036import org.jdrupes.builder.api.BuildException; 037import org.jdrupes.builder.api.Generator; 038import static org.jdrupes.builder.api.Intent.*; 039import org.jdrupes.builder.api.Project; 040import static org.jdrupes.builder.api.Project.Properties.*; 041import org.jdrupes.builder.api.Resource; 042import org.jdrupes.builder.api.ResourceRequest; 043import org.jdrupes.builder.api.Resources; 044import org.jdrupes.builder.core.AbstractGenerator; 045import static org.jdrupes.builder.mvnrepo.MvnProperties.*; 046import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 047 048/// A [Generator] (mainly) for POM files. In response to requests for 049/// [PomFile] this generator produces a Maven [Model] containing basic 050/// project information. The following properties are used: 051/// 052/// * The groupId is set to the value of the property 053/// [MvnProperties#GroupId] if it is defined. 054/// 055/// * The artifactId is set to the property [MvnProperties#ArtifactId], 056/// or to the name of the project if [MvnProperties#ArtifactId] is 057/// not defined. 058/// 059/// * The version is set to the value of the property 060/// [Project.Properties#Version]. 061/// 062/// Dependencies in the model are evaluated by querying the project's 063/// providers. 064/// 065/// Compile dependencies are evaluated as follows: 066/// 067/// * Resources of type [MvnRepoDependency] are obtained from providers 068/// associated via `Supply` and `Expose` that are not projects. These 069/// resources are Maven repository dependencies declared by the 070/// project itself. 071/// 072/// * Projects associated via `Supply` and `Expose` dependencies 073/// are then queried for resources of type [MvnRepoDependency] 074/// using their `Supply` dependencies. This yields the Maven 075/// repository coordinates of projects required for compilation. 076/// 077/// Runtime dependencies are evaluated as follows: 078/// 079/// * Resources of type [MvnRepoDependency] are obtained from providers 080/// associated via `Reveal` that are not projects. These 081/// resources represent Maven repository dependencies declared by 082/// the project itself. 083/// 084/// * Projects associated via `Reveal` dependencies are then queried for 085/// resources of type [MvnRepoDependency] using their `Supply` 086/// dependencies. This yields the Maven repository coordinates of 087/// projects required at runtime. 088/// 089/// The resulting model is passed to [#adaptPom] for project specific 090/// customization before it is written to the POM file. 091/// 092/// In addition to handling [PomFile] requests, this generator also 093/// responds to requests for [MvnRepoDependencies][MvnRepoDependency]. 094/// In this case, it returns Maven coordinates derived from the 095/// properties [MvnProperties#GroupId], [MvnProperties#ArtifactId] 096/// and [Project.Properties#Version] (see above). This reflects the 097/// assumption that a project with a POM file is intended to be released 098/// as a Maven artifact. Other projects in a multi-project build will 099/// therefore ultimately depend on the Maven artifact to be released. 100/// 101@SuppressWarnings("PMD.TooManyStaticImports") 102public class PomFileGenerator extends AbstractGenerator { 103 104 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 105 /// The Constant GENERATED_BY. 106 public static final String GENERATED_BY = "Generated by JDrupes Builder"; 107 private Supplier<Path> destination 108 = () -> project().buildDirectory().resolve("publications/maven"); 109 private Consumer<Model> pomAdapter = _ -> { 110 }; 111 112 /// Instantiates a new library generator. 113 /// 114 /// @param project the project 115 /// 116 public PomFileGenerator(Project project) { 117 super(project); 118 } 119 120 /// Returns the destination directory. Defaults to sub directory 121 /// `publications/maven` in the project's build directory 122 /// (see [Project#buildDirectory]). 123 /// 124 /// @return the destination 125 /// 126 public Path destination() { 127 return destination.get(); 128 } 129 130 /// Sets the destination directory. The [Path] is resolved against 131 /// the project's build directory (see [Project#buildDirectory]). 132 /// 133 /// @param destination the new destination 134 /// @return the jar generator 135 /// 136 public PomFileGenerator destination(Path destination) { 137 this.destination 138 = () -> project().buildDirectory().resolve(destination); 139 return this; 140 } 141 142 /// Sets the destination directory. 143 /// 144 /// @param destination the new destination 145 /// @return the jar generator 146 /// 147 public PomFileGenerator destination(Supplier<Path> destination) { 148 this.destination = destination; 149 return this; 150 } 151 152 @Override 153 protected <T extends Resource> Stream<T> 154 doProvide(ResourceRequest<T> requested) { 155 var pomPath = destination().resolve(Optional.ofNullable( 156 project().get(ArtifactId)).orElse(project().name()) + "-pom.xml"); 157 if (cleanup(requested, pomPath)) { 158 return Stream.empty(); 159 } 160 if (requested.accepts(MvnRepoDependencyType)) { 161 return generateRepoDependency(requested); 162 } 163 if (!requested.accepts(PomFileType)) { 164 return Stream.empty(); 165 } 166 167 // Always provide special type 168 if (!requested.type().equals(PomFileType)) { 169 @SuppressWarnings("unchecked") 170 var result = (Stream<T>) resources(of(PomFileType)); 171 return result; 172 } 173 174 pomPath.getParent().toFile().mkdirs(); 175 Model model = generatePom(); 176 177 // create, compare and maybe write model 178 pomPath.getParent().toFile().mkdirs(); 179 try { 180 // Create new in memory 181 ByteArrayOutputStream newPom = new ByteArrayOutputStream(); 182 new DefaultModelWriter().write(newPom, null, model); 183 newPom.close(); 184 185 // Read existing 186 var existingContent = pomPath.toFile().exists() 187 ? Files.readAllBytes(pomPath) 188 : new byte[0]; 189 190 // Compare and write 191 if (Arrays.equals(newPom.toByteArray(), existingContent)) { 192 logger.atFine().log("Existing %s is up to date.", 193 lazy(() -> project().rootProject().directory() 194 .relativize(pomPath))); 195 } else { 196 logger.atFine().log("Updating %s", 197 lazy(() -> project().rootProject() 198 .directory().relativize(pomPath))); 199 Files.write(pomPath, newPom.toByteArray()); 200 } 201 } catch (IOException e) { 202 throw new BuildException().from(this).cause(e); 203 } 204 205 @SuppressWarnings("unchecked") 206 var result = (Stream<T>) Stream.of(PomFile.of(pomPath)); 207 return result; 208 } 209 210 private Model generatePom() { 211 Model model = new Model(); 212 model.setModelVersion("4.0.0"); 213 model.setDependencyManagement(new DependencyManagement()); 214 var groupId = project().<String> get(GroupId); 215 if (groupId != null) { 216 model.setGroupId(groupId); 217 } 218 model.setArtifactId(Optional.ofNullable(project() 219 .<String> get(ArtifactId)).orElse(project().name())); 220 model.setVersion(project().get(Version)); 221 model.setName(project().name()); 222 223 // Get declared repository dependencies. These are the 224 // explicitly declared dependencies and the repository 225 // dependencies supplied by the projects on the compile 226 // or runtime classpath. 227 var compilationDeps = Resources.of(MvnRepoDependenciesType) 228 .addAll(project().providers(Supply, Expose).without(this) 229 .without(Project.class).resources(of(MvnRepoDependencyType))) 230 .addAll(project().providers() 231 .filter(p -> p instanceof Project).select(Supply, Expose) 232 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 233 .using(Supply)))); 234 addDependencies(model, compilationDeps, "compile"); 235 var runtimeDeps = Resources.of(MvnRepoDependenciesType) 236 .addAll(project().providers(Reveal).without(this) 237 .without(Project.class).resources(of(MvnRepoDependencyType))) 238 .addAll(project().providers() 239 .filter(p -> p instanceof Project).select(Reveal) 240 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 241 .using(Supply)))); 242 addDependencies(model, runtimeDeps, "runtime"); 243 244 // Adapt 245 pomAdapter.accept(model); 246 return model; 247 } 248 249 private void addDependencies(Model model, Resources<MvnRepoDependency> deps, 250 String scope) { 251 deps.stream().forEach(d -> { 252 var dep = new Dependency(); 253 dep.setGroupId(d.groupId()); 254 dep.setArtifactId(d.artifactId()); 255 dep.setVersion(d.version()); 256 if (d instanceof MvnRepoBom) { 257 dep.setScope("import"); 258 dep.setType("pom"); 259 model.getDependencyManagement().addDependency(dep); 260 } else { 261 dep.setScope(scope); 262 model.addDependency(dep); 263 } 264 }); 265 } 266 267 /// Allow derived classes to post process the generated POM. 268 /// 269 /// @param adaptor the adaptor 270 /// @return the pom file generator 271 /// 272 public PomFileGenerator adaptPom(Consumer<Model> adaptor) { 273 this.pomAdapter = adaptor; 274 return this; 275 } 276 277 @SuppressWarnings("unchecked") 278 private <T extends Resource> Stream<T> 279 generateRepoDependency(ResourceRequest<T> requested) { 280 if (requested.accepts(MvnRepoDependenciesType)) { 281 return Stream.empty(); 282 } 283 return Stream.of((T) MvnRepoDependency.of(String.format("%s:%s:%s", 284 project().<String> get(GroupId), Optional.ofNullable(project() 285 .<String> get(ArtifactId)).orElse(project().name()), 286 project().get(Version)))); 287 } 288 289}