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.java;
020
021import java.nio.file.Path;
022import java.util.Map;
023import java.util.Objects;
024import java.util.jar.Attributes;
025import java.util.stream.Stream;
026import org.jdrupes.builder.api.BuildException;
027import org.jdrupes.builder.api.Generator;
028import org.jdrupes.builder.api.IOResource;
029import static org.jdrupes.builder.api.Intent.*;
030import org.jdrupes.builder.api.Project;
031import org.jdrupes.builder.api.Resource;
032import org.jdrupes.builder.api.ResourceProvider;
033import org.jdrupes.builder.api.ResourceRequest;
034import org.jdrupes.builder.api.ResourceRetriever;
035import org.jdrupes.builder.api.ResourceType;
036import static org.jdrupes.builder.api.ResourceType.*;
037import org.jdrupes.builder.api.Resources;
038import org.jdrupes.builder.core.StreamCollector;
039import static org.jdrupes.builder.java.JavaTypes.*;
040
041/// A [Generator] for Java libraries packaged as jars. A library jar
042/// is expected to contain class files and supporting resources together
043/// with additional information in `META-INF/`.
044///
045/// The generator provides two types of resources.
046/// 
047/// 1. A [JarFile]. This type of resource is also returned if a more
048///    general [ResourceType] such as [ClasspathElement] is requested.
049///
050/// 2. An [AppJarFile]. When requesting this special jar type, the
051///    generator checks if a main class is specified.
052///
053/// In addition to explicitly adding resources, this generator supports
054/// resource retrieval from added providers. The resources of type [ClassTree]
055/// and [JavaResourceTree] that the providers supply will be used in
056/// addition to the explicitly added resources.
057///
058/// The standard pattern for creating a library is simply:
059/// ```java
060/// generator(LibraryGenerator::new).from(this);
061/// ```
062///
063public class LibraryBuilder extends JarBuilder
064        implements ResourceRetriever {
065
066    private final StreamCollector<ResourceProvider> providers
067        = StreamCollector.cached();
068    private String mainClass;
069
070    /// Instantiates a new library generator.
071    ///
072    /// @param project the project
073    ///
074    public LibraryBuilder(Project project) {
075        super(project, LibraryJarFileType);
076    }
077
078    @Override
079    public LibraryBuilder name(String name) {
080        rename(name);
081        return this;
082    }
083
084    /// Returns the main class.
085    ///
086    /// @return the main class
087    ///
088    public String mainClass() {
089        return mainClass;
090    }
091
092    /// Sets the main class.
093    ///
094    /// @param mainClass the new main class
095    /// @return the jar generator for method chaining
096    ///
097    public LibraryBuilder mainClass(String mainClass) {
098        this.mainClass = Objects.requireNonNull(mainClass);
099        return this;
100    }
101
102    /// Additionally uses the given providers for obtaining contents for the
103    /// jar.
104    ///
105    /// @param providers the providers
106    /// @return the jar generator
107    ///
108    @Override
109    public LibraryBuilder addFrom(ResourceProvider... providers) {
110        addFrom(Stream.of(providers));
111        return this;
112    }
113
114    /// Additionally uses the given providers for obtaining contents for the
115    /// jar.
116    ///
117    /// @param providers the providers
118    /// @return the jar generator
119    ///
120    @Override
121    public LibraryBuilder addFrom(Stream<ResourceProvider> providers) {
122        this.providers.add(providers.filter(p -> !p.equals(this)));
123        return this;
124    }
125
126    /// return the cached providers.
127    ///
128    /// @return the cached stream
129    ///
130    protected StreamCollector<ResourceProvider> providers() {
131        return providers;
132    }
133
134    @Override
135    protected void collectContents(Map<Path, Resources<IOResource>> contents) {
136        super.collectContents(contents);
137        // Add main class if defined
138        if (mainClass() != null) {
139            attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass()));
140        }
141        collectFromProviders(contents);
142    }
143
144    /// Collects the contents from the providers. This implementation
145    /// requests [ClassTree]s and [JavaResourceTree]s.
146    ///
147    /// @param contents the contents
148    ///
149    protected void collectFromProviders(
150            Map<Path, Resources<IOResource>> contents) {
151        providers().stream().map(
152            p -> p.resources(of(ClassTree.class).using(Supply)))
153            .flatMap(s -> s).parallel().forEach(t -> collect(contents, t));
154        providers().stream().map(p -> p.resources(
155            of(JavaResourceTree.class).using(Supply)))
156            .flatMap(s -> s).parallel().forEach(t -> collect(contents, t));
157    }
158
159    @Override
160    @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked",
161        "PMD.CyclomaticComplexity" })
162    protected <T extends Resource> Stream<T>
163            doProvide(ResourceRequest<T> requested) {
164        if (!requested.accepts(LibraryJarFileType)
165            && !requested.accepts(CleanlinessType)) {
166            return Stream.empty();
167        }
168
169        // Maybe only delete
170        if (requested.accepts(CleanlinessType)) {
171            destination().resolve(jarName()).toFile().delete();
172            return Stream.empty();
173        }
174
175        // Upgrade to most specific type to avoid duplicate generation
176        if (mainClass() != null && !requested.type().equals(AppJarFileType)) {
177            return (Stream<T>) context()
178                .resources(this, project().of(AppJarFileType));
179        }
180        if (mainClass() == null && !requested.type().equals(JarFileType)) {
181            return (Stream<T>) context()
182                .resources(this, project().of(JarFileType));
183        }
184
185        // Make sure mainClass is set for app jar
186        if (requested.requires(AppJarFileType) && mainClass() == null) {
187            throw new BuildException("Main class must be set for %s",
188                name()).from(this);
189        }
190
191        // Prepare jar file
192        var destDir = destination();
193        if (!destDir.toFile().exists()) {
194            if (!destDir.toFile().mkdirs()) {
195                throw new BuildException("Cannot create directory " + destDir);
196            }
197        }
198        var jarResource = requested.requires(AppJarFileType)
199            ? project().newResource(AppJarFileType,
200                destDir.resolve(jarName()))
201            : project().newResource(LibraryJarFileType,
202                destDir.resolve(jarName()));
203
204        buildJar(jarResource);
205        return Stream.of((T) jarResource);
206    }
207}