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