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