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/// 105public class PomFileGenerator extends AbstractGenerator { 106 107 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 108 /// The Constant GENERATED_BY. 109 public static final String GENERATED_BY = "Generated by JDrupes Builder"; 110 private Supplier<Path> destination 111 = () -> project().buildDirectory().resolve("publications/maven"); 112 private Consumer<Model> pomAdapter = _ -> { 113 }; 114 115 /// Instantiates a new library generator. 116 /// 117 /// @param project the project 118 /// 119 public PomFileGenerator(Project project) { 120 super(project); 121 } 122 123 /// Returns the destination directory. Defaults to sub directory 124 /// `publications/maven` in the project's build directory 125 /// (see [Project#buildDirectory]). 126 /// 127 /// @return the destination 128 /// 129 public Path destination() { 130 return destination.get(); 131 } 132 133 /// Sets the destination directory. The [Path] is resolved against 134 /// the project's build directory (see [Project#buildDirectory]). 135 /// 136 /// @param destination the new destination 137 /// @return the POM file generator 138 /// 139 public PomFileGenerator destination(Path destination) { 140 this.destination 141 = () -> project().buildDirectory().resolve(destination); 142 return this; 143 } 144 145 /// Sets the destination directory. 146 /// 147 /// @param destination the new destination 148 /// @return the POM file generator 149 /// 150 public PomFileGenerator destination(Supplier<Path> destination) { 151 this.destination = destination; 152 return this; 153 } 154 155 @Override 156 protected <T extends Resource> Collection<T> 157 doProvide(ResourceRequest<T> requested) { 158 var pomPath = destination().resolve(Optional.ofNullable( 159 project().get(ArtifactId)).orElse(project().name()) + "-pom.xml"); 160 if (cleanup(requested, pomPath)) { 161 return Collections.emptyList(); 162 } 163 if (requested.accepts(MvnRepoDependencyType)) { 164 return generateRepoDependency(requested); 165 } 166 if (!requested.accepts(PomFileType)) { 167 return Collections.emptyList(); 168 } 169 170 // Always provide special type 171 if (!requested.type().equals(PomFileType)) { 172 @SuppressWarnings("unchecked") 173 var result = (Collection<T>) resources(of(PomFileType)).toList(); 174 return result; 175 } 176 177 pomPath.getParent().toFile().mkdirs(); 178 Model model = generatePom(); 179 180 // create, compare and maybe write model 181 pomPath.getParent().toFile().mkdirs(); 182 try { 183 // Create new in memory 184 ByteArrayOutputStream newPom = new ByteArrayOutputStream(); 185 new DefaultModelWriter().write(newPom, null, model); 186 newPom.close(); 187 188 // Read existing 189 var existingContent = pomPath.toFile().exists() 190 ? Files.readAllBytes(pomPath) 191 : new byte[0]; 192 193 // Compare and write 194 if (Arrays.equals(newPom.toByteArray(), existingContent)) { 195 logger.atFine().log("Existing %s is up to date.", 196 lazy(() -> project().rootProject().directory() 197 .relativize(pomPath))); 198 } else { 199 logger.atFine().log("Updating %s", 200 lazy(() -> project().rootProject() 201 .directory().relativize(pomPath))); 202 Files.write(pomPath, newPom.toByteArray()); 203 } 204 } catch (IOException e) { 205 throw new BuildException().from(this).cause(e); 206 } 207 208 @SuppressWarnings("unchecked") 209 var result = (Collection<T>) List.of(PomFile.of(pomPath)); 210 return result; 211 } 212 213 private Model generatePom() { 214 Model model = new Model(); 215 model.setModelVersion("4.0.0"); 216 model.setDependencyManagement(new DependencyManagement()); 217 var groupId = project().get(GroupId); 218 if (groupId != null) { 219 model.setGroupId(groupId); 220 } 221 model.setArtifactId(Optional.ofNullable(project() 222 .get(ArtifactId)).orElse(project().name())); 223 model.setVersion(project().get(Version)); 224 model.setName(project().name()); 225 226 // Get declared repository dependencies. These are the 227 // explicitly declared dependencies and the repository 228 // dependencies supplied by the projects on the compile 229 // or runtime classpath. 230 var compilationDeps = Resources.of(MvnRepoDependenciesType) 231 .addAll(project().providers(Supply, Expose).without(this) 232 .without(Project.class).resources(of(MvnRepoDependencyType))) 233 .addAll(project().providers() 234 .filter(p -> p instanceof Project).select(Supply, Expose) 235 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 236 .using(Supply)))); 237 addDependencies(model, compilationDeps, "compile"); 238 var runtimeDeps = Resources.of(MvnRepoDependenciesType) 239 .addAll(project().providers(Reveal).without(this) 240 .without(Project.class).resources(of(MvnRepoDependencyType))) 241 .addAll(project().providers() 242 .filter(p -> p instanceof Project).select(Reveal) 243 .flatMap(p -> p.resources(of(MvnRepoDependencyType) 244 .using(Supply)))); 245 addDependencies(model, runtimeDeps, "runtime"); 246 247 // Adapt 248 pomAdapter.accept(model); 249 return model; 250 } 251 252 private void addDependencies(Model model, Resources<MvnRepoDependency> deps, 253 String scope) { 254 deps.stream().forEach(d -> { 255 var dep = new Dependency(); 256 dep.setGroupId(d.groupId()); 257 dep.setArtifactId(d.artifactId()); 258 dep.setVersion(d.version()); 259 if (d instanceof MvnRepoBom) { 260 dep.setScope("import"); 261 dep.setType("pom"); 262 model.getDependencyManagement().addDependency(dep); 263 } else { 264 dep.setScope(scope); 265 model.addDependency(dep); 266 } 267 }); 268 } 269 270 /// Allow derived classes to post process the generated POM. 271 /// 272 /// @param adaptor the adaptor 273 /// @return the pom file generator 274 /// 275 public PomFileGenerator adaptPom(Consumer<Model> adaptor) { 276 this.pomAdapter = adaptor; 277 return this; 278 } 279 280 @SuppressWarnings("unchecked") 281 private <T extends Resource> Collection<T> 282 generateRepoDependency(ResourceRequest<T> requested) { 283 if (requested.accepts(MvnRepoDependenciesType)) { 284 return Collections.emptyList(); 285 } 286 return List.of((T) MvnRepoDependency.of(String.format("%s:%s:%s", 287 project().<String> get(GroupId), Optional.ofNullable(project() 288 .<String> get(ArtifactId)).orElse(project().name()), 289 project().get(Version)))); 290 } 291 292}