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.Model;
031import org.apache.maven.model.io.DefaultModelWriter;
032import org.jdrupes.builder.api.BuildException;
033import org.jdrupes.builder.api.Generator;
034import org.jdrupes.builder.api.Project;
035import static org.jdrupes.builder.api.Project.Properties.*;
036import org.jdrupes.builder.api.Resource;
037import org.jdrupes.builder.api.ResourceRequest;
038import org.jdrupes.builder.core.AbstractGenerator;
039import static org.jdrupes.builder.mvnrepo.MvnProperties.*;
040import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*;
041
042/// A [Generator] for POM files. The generator generates a maven
043/// [Model] with basic information. The group is set to the value
044/// of the property [MvnProperties#GroupId] if it is defined. The
045/// artifact id is set to the property [MvnProperties#ArtifactId]
046/// or the name of the project, if [MvnProperties#ArtifactId] is not
047/// defined. The version is set to the value of the property
048/// [Project.Properties#Version].
049///
050/// This basic model is passed to [#adaptPom] where it can be adapted.
051/// as the project requires. Finally, the model is written to the
052/// POM file
053///
054public class PomFileGenerator extends AbstractGenerator {
055
056    /// The Constant GENERATED_BY.
057    public static final String GENERATED_BY = "Generated by JDrupes Builder";
058    private Supplier<Path> destination
059        = () -> project().buildDirectory().resolve("publications/maven");
060    private Consumer<Model> pomAdapter = _ -> {
061    }; // Do nothing>
062
063    /// Instantiates a new library generator.
064    ///
065    /// @param project the project
066    ///
067    public PomFileGenerator(Project project) {
068        super(project);
069    }
070
071    /// Returns the destination directory. Defaults to sub directory
072    /// `publications/maven` in the project's build directory
073    /// (see [Project#buildDirectory]).
074    ///
075    /// @return the destination
076    ///
077    public Path destination() {
078        return destination.get();
079    }
080
081    /// Sets the destination directory. The [Path] is resolved against
082    /// the project's build directory (see [Project#buildDirectory]).
083    ///
084    /// @param destination the new destination
085    /// @return the jar generator
086    ///
087    public PomFileGenerator destination(Path destination) {
088        this.destination
089            = () -> project().buildDirectory().resolve(destination);
090        return this;
091    }
092
093    /// Sets the destination directory.
094    ///
095    /// @param destination the new destination
096    /// @return the jar generator
097    ///
098    public PomFileGenerator destination(Supplier<Path> destination) {
099        this.destination = destination;
100        return this;
101    }
102
103    @Override
104    protected <T extends Resource> Stream<T>
105            doProvide(ResourceRequest<T> requested) {
106        var pomPath = destination().resolve(Optional.ofNullable(
107            project().get(ArtifactId)).orElse(project().name()) + "-pom.xml");
108        if (cleanup(requested, pomPath)) {
109            return Stream.empty();
110        }
111
112        if (!requested.includes(PomFileType)) {
113            return Stream.empty();
114        }
115
116        pomPath.getParent().toFile().mkdirs();
117        var deps = project().newResource(MvnRepoDependenciesType).addAll(
118            project().supplied(new ResourceRequest<>(
119                MvnRepoDependenciesType)));
120        Model model = generatePom();
121
122        // create, compare and maybe write model
123        pomPath.getParent().toFile().mkdirs();
124        try {
125            // Create new in memory
126            ByteArrayOutputStream newPom = new ByteArrayOutputStream();
127            new DefaultModelWriter().write(newPom, null, model);
128            newPom.close();
129
130            // Read existing
131            var existingContent = pomPath.toFile().exists()
132                ? Files.readAllBytes(pomPath)
133                : new byte[0];
134
135            // Compare and write
136            if (Arrays.equals(newPom.toByteArray(), existingContent)) {
137                log.fine(() -> "Existing " + project().rootProject()
138                    .directory().relativize(pomPath) + " is up to date.");
139            } else {
140                log.fine(() -> "Updating " + project().rootProject()
141                    .directory().relativize(pomPath) + ".");
142                Files.write(pomPath, newPom.toByteArray());
143            }
144        } catch (IOException e) {
145            throw new BuildException(e);
146        }
147
148        @SuppressWarnings("unchecked")
149        var result = (Stream<T>) Stream
150            .of(project().newResource(PomFileType, pomPath));
151        return result;
152    }
153
154    private Model generatePom() {
155        Model model = new Model();
156        model.setModelVersion("4.0.0");
157        var groupId = project().<String> get(GroupId);
158        if (groupId != null) {
159            model.setGroupId(groupId);
160        }
161        model.setArtifactId(Optional.ofNullable(project()
162            .<String> get(ArtifactId)).orElse(project().name()));
163        model.setVersion(project().get(Version));
164        model.setName(project().name());
165
166        // Adapt
167        pomAdapter.accept(model);
168        return model;
169    }
170
171    /// Allow derived classes to post process the generated POM.
172    ///
173    /// @param adaptor the adaptor
174    /// @return the pom file generator
175    ///
176    public PomFileGenerator adaptPom(Consumer<Model> adaptor) {
177        this.pomAdapter = adaptor;
178        return this;
179    }
180
181}