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}