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 java.io.ByteArrayOutputStream; 022import java.io.IOException; 023import java.nio.file.Files; 024import java.nio.file.Path; 025import java.util.Arrays; 026import java.util.Optional; 027import java.util.function.Consumer; 028import java.util.function.Supplier; 029import java.util.stream.Stream; 030import org.apache.maven.model.Dependency; 031import org.apache.maven.model.Model; 032import org.apache.maven.model.io.DefaultModelWriter; 033import org.jdrupes.builder.api.BuildException; 034import org.jdrupes.builder.api.Generator; 035import static org.jdrupes.builder.api.Intend.*; 036import org.jdrupes.builder.api.Project; 037import static org.jdrupes.builder.api.Project.Properties.*; 038import org.jdrupes.builder.api.Resource; 039import org.jdrupes.builder.api.ResourceRequest; 040import static org.jdrupes.builder.api.ResourceRequest.*; 041import org.jdrupes.builder.api.Resources; 042import org.jdrupes.builder.core.AbstractGenerator; 043import static org.jdrupes.builder.mvnrepo.MvnProperties.*; 044import org.jdrupes.builder.mvnrepo.MvnRepoDependency.Scope; 045import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 046 047/// A [Generator] (mainly) for POM files. In response to requests for 048/// [PomFile] it generates a maven [Model] with basic information. 049/// The group is set to the value of the property [MvnProperties#GroupId] 050/// if it is defined. The artifact id is set to the property 051/// [MvnProperties#ArtifactId] or the name of the project, if 052/// [MvnProperties#ArtifactId] is not defined. The version is set to 053/// the value of the property [Project.Properties#Version]. 054/// 055/// This basic model is passed to [#adaptPom] where it can be adapted. 056/// as the project requires. Finally, the model is written to the 057/// POM file. 058/// 059/// In addition to the [PomFile] requests, this generator also handles 060/// requests for [MvnRepoDependency]. This reflects that a project that 061/// has a POM file is obviously intended to be released as a maven 062/// artifact. Other projects in a multi-project build will therefore 063/// eventually depend on the maven artifact "to be released". 064/// 065@SuppressWarnings("PMD.TooManyStaticImports") 066public class PomFileGenerator extends AbstractGenerator { 067 068 /// The Constant GENERATED_BY. 069 public static final String GENERATED_BY = "Generated by JDrupes Builder"; 070 private Supplier<Path> destination 071 = () -> project().buildDirectory().resolve("publications/maven"); 072 private Consumer<Model> pomAdapter = _ -> { 073 }; // Do nothing> 074 private boolean runtimeLibrary; 075 076 /// Instantiates a new library generator. 077 /// 078 /// @param project the project 079 /// 080 public PomFileGenerator(Project project) { 081 super(project); 082 } 083 084 /// Returns the destination directory. Defaults to sub directory 085 /// `publications/maven` in the project's build directory 086 /// (see [Project#buildDirectory]). 087 /// 088 /// @return the destination 089 /// 090 public Path destination() { 091 return destination.get(); 092 } 093 094 /// Sets the destination directory. The [Path] is resolved against 095 /// the project's build directory (see [Project#buildDirectory]). 096 /// 097 /// @param destination the new destination 098 /// @return the jar generator 099 /// 100 public PomFileGenerator destination(Path destination) { 101 this.destination 102 = () -> project().buildDirectory().resolve(destination); 103 return this; 104 } 105 106 /// Sets the destination directory. 107 /// 108 /// @param destination the new destination 109 /// @return the jar generator 110 /// 111 public PomFileGenerator destination(Supplier<Path> destination) { 112 this.destination = destination; 113 return this; 114 } 115 116 /// POMs do not contain any information whether the library is 117 /// intended to be used at runtime or at compile time. This information 118 /// is only needed when this generator responds to requests for 119 /// [MvnRepoDependency]. 120 /// 121 /// @return the pom file generator 122 /// 123 public PomFileGenerator runtimeLibrary() { 124 runtimeLibrary = true; 125 return this; 126 } 127 128 @Override 129 protected <T extends Resource> Stream<T> 130 doProvide(ResourceRequest<T> requested) { 131 var pomPath = destination().resolve(Optional.ofNullable( 132 project().get(ArtifactId)).orElse(project().name()) + "-pom.xml"); 133 if (cleanup(requested, pomPath)) { 134 return Stream.empty(); 135 } 136 137 if (requested.accepts(MvnRepoCompilationDepsType)) { 138 return generateRepoDependency(requested); 139 } 140 141 if (!requested.collects(PomFileType)) { 142 return Stream.empty(); 143 } 144 145 pomPath.getParent().toFile().mkdirs(); 146 var deps = newResource(MvnRepoCompilationDepsType) 147 .addAll(project().providers(Supply, Expose, Consume) 148 // Don't add dependency on self 149 .filter(p -> !p.equals(this)).flatMap( 150 p -> project().context().get(p, requestFor( 151 MvnRepoCompilationDepsType)))); 152 Model model = generatePom(deps); 153 154 // create, compare and maybe write model 155 pomPath.getParent().toFile().mkdirs(); 156 try { 157 // Create new in memory 158 ByteArrayOutputStream newPom = new ByteArrayOutputStream(); 159 new DefaultModelWriter().write(newPom, null, model); 160 newPom.close(); 161 162 // Read existing 163 var existingContent = pomPath.toFile().exists() 164 ? Files.readAllBytes(pomPath) 165 : new byte[0]; 166 167 // Compare and write 168 if (Arrays.equals(newPom.toByteArray(), existingContent)) { 169 log.fine(() -> "Existing " + project().rootProject() 170 .directory().relativize(pomPath) + " is up to date."); 171 } else { 172 log.fine(() -> "Updating " + project().rootProject() 173 .directory().relativize(pomPath) + "."); 174 Files.write(pomPath, newPom.toByteArray()); 175 } 176 } catch (IOException e) { 177 throw new BuildException(e); 178 } 179 180 @SuppressWarnings("unchecked") 181 var result = (Stream<T>) Stream 182 .of(project().newResource(PomFileType, pomPath)); 183 return result; 184 } 185 186 private Model generatePom(Resources<MvnRepoDependency> deps) { 187 Model model = new Model(); 188 model.setModelVersion("4.0.0"); 189 var groupId = project().<String> get(GroupId); 190 if (groupId != null) { 191 model.setGroupId(groupId); 192 } 193 model.setArtifactId(Optional.ofNullable(project() 194 .<String> get(ArtifactId)).orElse(project().name())); 195 model.setVersion(project().get(Version)); 196 model.setName(project().name()); 197 deps.stream().forEach(d -> { 198 var dep = new Dependency(); 199 dep.setGroupId(d.groupId()); 200 dep.setArtifactId(d.artifactId()); 201 dep.setVersion(d.version()); 202 dep.setScope(d.scope() == Scope.Compile ? "compile" : "runtime"); 203 model.addDependency(dep); 204 }); 205 206 // Adapt 207 pomAdapter.accept(model); 208 return model; 209 } 210 211 /// Allow derived classes to post process the generated POM. 212 /// 213 /// @param adaptor the adaptor 214 /// @return the pom file generator 215 /// 216 public PomFileGenerator adaptPom(Consumer<Model> adaptor) { 217 this.pomAdapter = adaptor; 218 return this; 219 } 220 221 @SuppressWarnings("unchecked") 222 private <T extends Resource> Stream<T> 223 generateRepoDependency(ResourceRequest<T> requested) { 224 if (requested.accepts(MvnRepoRuntimeDepsType) && !runtimeLibrary) { 225 return Stream.empty(); 226 } 227 return Stream.of((T) newResource(MvnRepoDependencyType, 228 project().<String> get(GroupId) 229 + ":" + Optional.ofNullable(project() 230 .<String> get(ArtifactId)).orElse(project().name()) 231 + ":" + project().get(Version), 232 runtimeLibrary ? Scope.Runtime : Scope.Compile)); 233 } 234 235}