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