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 161 if (requested.accepts(MvnRepoDependencyType)) { 162 return generateRepoDependency(requested); 163 } 164 165 if (!requested.accepts(PomFileType)) { 166 return Stream.empty(); 167 } 168 169 pomPath.getParent().toFile().mkdirs(); 170 Model model = generatePom(); 171 172 // create, compare and maybe write model 173 pomPath.getParent().toFile().mkdirs(); 174 try { 175 // Create new in memory 176 ByteArrayOutputStream newPom = new ByteArrayOutputStream(); 177 new DefaultModelWriter().write(newPom, null, model); 178 newPom.close(); 179 180 // Read existing 181 var existingContent = pomPath.toFile().exists() 182 ? Files.readAllBytes(pomPath) 183 : new byte[0]; 184 185 // Compare and write 186 if (Arrays.equals(newPom.toByteArray(), existingContent)) { 187 logger.atFine().log("Existing %s is up to date.", 188 lazy(() -> project().rootProject().directory() 189 .relativize(pomPath))); 190 } else { 191 logger.atFine().log("Updating %s", 192 lazy(() -> project().rootProject() 193 .directory().relativize(pomPath))); 194 Files.write(pomPath, newPom.toByteArray()); 195 } 196 } catch (IOException e) { 197 throw new BuildException().from(this).cause(e); 198 } 199 200 @SuppressWarnings("unchecked") 201 var result = (Stream<T>) Stream 202 .of(project().newResource(PomFileType, pomPath)); 203 return result; 204 } 205 206 private Model generatePom() { 207 Model model = new Model(); 208 model.setModelVersion("4.0.0"); 209 model.setDependencyManagement(new DependencyManagement()); 210 var groupId = project().<String> get(GroupId); 211 if (groupId != null) { 212 model.setGroupId(groupId); 213 } 214 model.setArtifactId(Optional.ofNullable(project() 215 .<String> get(ArtifactId)).orElse(project().name())); 216 model.setVersion(project().get(Version)); 217 model.setName(project().name()); 218 219 // Get declared repository dependencies. These are the 220 // explicitly declared dependencies and the repository 221 // dependencies supplied by the projects on the compile 222 // or runtime classpath. 223 var compilationDeps = newResource(MvnRepoDependenciesType) 224 .addAll(project().providers(Supply, Expose).without(this) 225 .without(Project.class).resources(of(MvnRepoDependencyType))) 226 .addAll(project().providers() 227 .filter(p -> p instanceof Project).select(Supply, Expose) 228 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 229 .using(Supply)))); 230 addDependencies(model, compilationDeps, "compile"); 231 var runtimeDeps = newResource(MvnRepoDependenciesType) 232 .addAll(project().providers(Reveal).without(this) 233 .without(Project.class).resources(of(MvnRepoDependencyType))) 234 .addAll(project().providers() 235 .filter(p -> p instanceof Project).select(Reveal) 236 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 237 .using(Supply)))); 238 addDependencies(model, runtimeDeps, "runtime"); 239 240 // Adapt 241 pomAdapter.accept(model); 242 return model; 243 } 244 245 private void addDependencies(Model model, Resources<MvnRepoDependency> deps, 246 String scope) { 247 deps.stream().forEach(d -> { 248 var dep = new Dependency(); 249 dep.setGroupId(d.groupId()); 250 dep.setArtifactId(d.artifactId()); 251 dep.setVersion(d.version()); 252 if (d instanceof MvnRepoBom) { 253 dep.setScope("import"); 254 dep.setType("pom"); 255 model.getDependencyManagement().addDependency(dep); 256 } else { 257 dep.setScope(scope); 258 model.addDependency(dep); 259 } 260 }); 261 } 262 263 /// Allow derived classes to post process the generated POM. 264 /// 265 /// @param adaptor the adaptor 266 /// @return the pom file generator 267 /// 268 public PomFileGenerator adaptPom(Consumer<Model> adaptor) { 269 this.pomAdapter = adaptor; 270 return this; 271 } 272 273 @SuppressWarnings("unchecked") 274 private <T extends Resource> Stream<T> 275 generateRepoDependency(ResourceRequest<T> requested) { 276 if (requested.accepts(MvnRepoDependenciesType)) { 277 return Stream.empty(); 278 } 279 return Stream.of((T) newResource(MvnRepoDependencyType, 280 project().<String> get(GroupId) 281 + ":" + Optional.ofNullable(project() 282 .<String> get(ArtifactId)).orElse(project().name()) 283 + ":" + project().get(Version))); 284 } 285 286}