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