001/*
002 * JDrupes Builder
003 * Copyright (C) 2025, 2026 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.java;
020
021import com.google.common.flogger.FluentLogger;
022import java.nio.file.Path;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.jar.Attributes;
029import java.util.stream.Stream;
030import org.jdrupes.builder.api.BuildException;
031import org.jdrupes.builder.api.ConfigurationException;
032import org.jdrupes.builder.api.Generator;
033import org.jdrupes.builder.api.InputResource;
034import org.jdrupes.builder.api.Intent;
035import static org.jdrupes.builder.api.Intent.*;
036import org.jdrupes.builder.api.Project;
037import org.jdrupes.builder.api.Resource;
038import org.jdrupes.builder.api.ResourceProvider;
039import org.jdrupes.builder.api.ResourceRequest;
040import org.jdrupes.builder.api.ResourceRetriever;
041import org.jdrupes.builder.api.ResourceType;
042import static org.jdrupes.builder.api.ResourceType.*;
043import org.jdrupes.builder.api.Resources;
044import org.jdrupes.builder.core.StreamCollector;
045import static org.jdrupes.builder.java.JavaTypes.*;
046
047/// A [Generator] for Java libraries packaged as jars. A library jar
048/// is expected to contain class files and supporting resources together
049/// with additional information in `META-INF/`.
050///
051/// The generator provides two types of resources.
052///
053/// 1. A [LibraryJarFile]. This type of resource is also returned if a more
054///    general [ResourceType] such as [CodeContribution] is requested.
055///
056/// 2. An [AppJarFile]. When requesting this special jar type, the
057///    generator checks if a main class is specified.
058///
059/// In addition to explicitly adding resources, this generator supports
060/// resource retrieval from added providers. The resources of type [ClassTree]
061/// and [JavaResourceTree] that the providers added with
062/// [ResourceRetriever#addFrom(ResourceProvider...)] [supply][Intent#Supply]
063/// are included in the library in addition to the explicitly added resources.
064///
065/// The enables the simple standard pattern for creating a library:
066/// ```java
067/// generator(LibraryBuilder::new).addFrom(this);
068/// ```
069///
070/// ## JPMS support
071///
072/// When the compiled classes contain a `module-info.class`, the JAR is
073/// a named module. If no `module-info.class` is present but [moduleName]
074/// is set (or auto-detected from the project name), an `Automatic-Module-Name`
075/// manifest attribute is added so the JAR is treated as an automatic module
076/// on the module-path.
077///
078public class LibraryBuilder extends JarBuilder implements ResourceRetriever {
079
080    @SuppressWarnings({ "unused" })
081    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
082    private final StreamCollector<ResourceProvider> providers
083        = StreamCollector.cached();
084    private String mainClass;
085    private String moduleName;
086
087    /// Instantiates a new library generator.
088    ///
089    /// @param project the project
090    ///
091    public LibraryBuilder(Project project) {
092        super(project, LibraryJarFileType);
093    }
094
095    @Override
096    public LibraryBuilder name(String name) {
097        rename(name);
098        return this;
099    }
100
101    /// Returns the main class.
102    ///
103    /// @return the main class
104    ///
105    public String mainClass() {
106        return mainClass;
107    }
108
109    /// Sets the main class.
110    ///
111    /// @param mainClass the new main class
112    /// @return the library builder for method chaining
113    ///
114    public LibraryBuilder mainClass(String mainClass) {
115        this.mainClass = Objects.requireNonNull(mainClass);
116        return this;
117    }
118
119    /// Returns the module name for the `Automatic-Module-Name` manifest
120    /// attribute. If not set, the project name is sanitized for JPMS
121    /// requirements.
122    ///
123    /// @return the module name
124    ///
125    public String moduleName() {
126        return moduleName;
127    }
128
129    /// Sets the module name used for the `Automatic-Module-Name`
130    /// manifest attribute. This allows the JAR to be used as an
131    /// automatic module on the module-path.
132    ///
133    /// @param moduleName the module name
134    /// @return this builder
135    ///
136    public LibraryBuilder moduleName(String moduleName) {
137        this.moduleName = moduleName;
138        return this;
139    }
140
141    @Override
142    public LibraryBuilder addFrom(ResourceProvider... providers) {
143        addFrom(Stream.of(providers));
144        return this;
145    }
146
147    @Override
148    public LibraryBuilder addFrom(Stream<ResourceProvider> providers) {
149        this.providers.add(providers.filter(p -> !p.equals(this)));
150        return this;
151    }
152
153    /// return the cached providers.
154    ///
155    /// @return the content providers
156    ///
157    protected StreamCollector<ResourceProvider> contentProviders() {
158        return providers;
159    }
160
161    @Override
162    protected void
163            collectContents(Map<Path, Resources<InputResource>> contents) {
164        super.collectContents(contents);
165        // Add main class if defined
166        if (mainClass() != null) {
167            attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass()));
168        }
169
170        // Add Automatic-Module-Name if specified and not a named module
171        if (!contents.containsKey(Path.of("module-info.class"))
172            && moduleName() != null) {
173            attributes(Map.entry(
174                new Attributes.Name("Automatic-Module-Name"), moduleName()));
175        }
176        collectFromProviders(contents);
177    }
178
179    /// Collects the contents from the providers. This implementation
180    /// requests [ClassTree]s and [JavaResourceTree]s.
181    ///
182    /// @param contents the contents
183    ///
184    protected void collectFromProviders(
185            Map<Path, Resources<InputResource>> contents) {
186        contentProviders().stream()
187            .map(p -> p.resources(of(ClassTreeType).using(Supply)))
188            // Terminate to trigger all future stream evaluations before
189            // starting to process the results. Then collect in parallel.
190            .toList().stream().flatMap(s -> s).toList().parallelStream()
191            .forEach(t -> collect(contents, t));
192        contentProviders().stream()
193            .map(p -> p.resources(of(JavaResourceTreeType).using(Supply)))
194            // Terminate to trigger all future stream evaluations before
195            // starting to process the results. Then collect in parallel.
196            .toList().stream().flatMap(s -> s).toList().parallelStream()
197            .forEach(t -> collect(contents, t));
198    }
199
200    @Override
201    @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked",
202        "PMD.CyclomaticComplexity" })
203    protected <T extends Resource> Collection<T>
204            doProvide(ResourceRequest<T> request) {
205        if (!request.accepts(LibraryJarFileType)
206            && !request.accepts(CleanlinessType)) {
207            return Collections.emptyList();
208        }
209
210        // Maybe only delete
211        if (request.accepts(CleanlinessType)) {
212            destination().resolve(jarName()).toFile().delete();
213            return Collections.emptyList();
214        }
215
216        // Upgrade to most specific type to avoid duplicate generation
217        if (mainClass() != null && !request.type().equals(AppJarFileType)) {
218            return (Collection<T>) context()
219                .resources(this, project().of(AppJarFileType)).toList();
220        }
221        if (mainClass() == null && !request.type().equals(LibraryJarFileType)) {
222            return (Collection<T>) context()
223                .resources(this, project().of(LibraryJarFileType)).toList();
224        }
225
226        // Make sure mainClass is set for app jar
227        if (request.isFor(AppJarFileType) && mainClass() == null) {
228            throw new ConfigurationException().from(this).message(
229                "Main class must be set for %s", name());
230        }
231
232        // Prepare jar file
233        var destDir = destination();
234        if (!destDir.toFile().exists()) {
235            if (!destDir.toFile().mkdirs()) {
236                throw new BuildException().from(this)
237                    .message("Cannot create directory " + destDir);
238            }
239        }
240        var jarResource = request.isFor(AppJarFileType)
241            ? AppJarFile.of(destDir.resolve(jarName()))
242            : LibraryJarFile.of(destDir.resolve(jarName()));
243
244        buildJar(jarResource);
245        return List.of((T) jarResource);
246    }
247}