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.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 [ClasspathElement] 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///
070public class LibraryBuilder extends JarBuilder implements ResourceRetriever {
071
072    @SuppressWarnings({ "unused" })
073    private static final FluentLogger logger = FluentLogger.forEnclosingClass();
074    private final StreamCollector<ResourceProvider> providers
075        = StreamCollector.cached();
076    private String mainClass;
077
078    /// Instantiates a new library generator.
079    ///
080    /// @param project the project
081    ///
082    public LibraryBuilder(Project project) {
083        super(project, LibraryJarFileType);
084    }
085
086    @Override
087    public LibraryBuilder name(String name) {
088        rename(name);
089        return this;
090    }
091
092    /// Returns the main class.
093    ///
094    /// @return the main class
095    ///
096    public String mainClass() {
097        return mainClass;
098    }
099
100    /// Sets the main class.
101    ///
102    /// @param mainClass the new main class
103    /// @return the library builder for method chaining
104    ///
105    public LibraryBuilder mainClass(String mainClass) {
106        this.mainClass = Objects.requireNonNull(mainClass);
107        return this;
108    }
109
110    @Override
111    public LibraryBuilder addFrom(ResourceProvider... providers) {
112        addFrom(Stream.of(providers));
113        return this;
114    }
115
116    @Override
117    public LibraryBuilder addFrom(Stream<ResourceProvider> providers) {
118        this.providers.add(providers.filter(p -> !p.equals(this)));
119        return this;
120    }
121
122    /// return the cached providers.
123    ///
124    /// @return the content providers
125    ///
126    protected StreamCollector<ResourceProvider> contentProviders() {
127        return providers;
128    }
129
130    @Override
131    protected void
132            collectContents(Map<Path, Resources<InputResource>> contents) {
133        super.collectContents(contents);
134        // Add main class if defined
135        if (mainClass() != null) {
136            attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass()));
137        }
138        collectFromProviders(contents);
139    }
140
141    /// Collects the contents from the providers. This implementation
142    /// requests [ClassTree]s and [JavaResourceTree]s.
143    ///
144    /// @param contents the contents
145    ///
146    protected void collectFromProviders(
147            Map<Path, Resources<InputResource>> contents) {
148        contentProviders().stream()
149            .map(p -> p.resources(of(ClassTreeType).using(Supply)))
150            // Terminate to trigger all future stream evaluations before
151            // starting to process the results. Then collect in parallel.
152            .toList().stream().flatMap(s -> s).toList().parallelStream()
153            .forEach(t -> collect(contents, t));
154        contentProviders().stream()
155            .map(p -> p.resources(of(JavaResourceTreeType).using(Supply)))
156            // Terminate to trigger all future stream evaluations before
157            // starting to process the results. Then collect in parallel.
158            .toList().stream().flatMap(s -> s).toList().parallelStream()
159            .forEach(t -> collect(contents, t));
160    }
161
162    @Override
163    @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked",
164        "PMD.CyclomaticComplexity" })
165    protected <T extends Resource> Collection<T>
166            doProvide(ResourceRequest<T> request) {
167        if (!request.accepts(LibraryJarFileType)
168            && !request.accepts(CleanlinessType)) {
169            return Collections.emptyList();
170        }
171
172        // Maybe only delete
173        if (request.accepts(CleanlinessType)) {
174            destination().resolve(jarName()).toFile().delete();
175            return Collections.emptyList();
176        }
177
178        // Upgrade to most specific type to avoid duplicate generation
179        if (mainClass() != null && !request.type().equals(AppJarFileType)) {
180            return (Collection<T>) context()
181                .resources(this, project().of(AppJarFileType)).toList();
182        }
183        if (mainClass() == null && !request.type().equals(LibraryJarFileType)) {
184            return (Collection<T>) context()
185                .resources(this, project().of(LibraryJarFileType)).toList();
186        }
187
188        // Make sure mainClass is set for app jar
189        if (request.isFor(AppJarFileType) && mainClass() == null) {
190            throw new ConfigurationException().from(this).message(
191                "Main class must be set for %s", name());
192        }
193
194        // Prepare jar file
195        var destDir = destination();
196        if (!destDir.toFile().exists()) {
197            if (!destDir.toFile().mkdirs()) {
198                throw new BuildException().from(this)
199                    .message("Cannot create directory " + destDir);
200            }
201        }
202        var jarResource = request.isFor(AppJarFileType)
203            ? AppJarFile.of(destDir.resolve(jarName()))
204            : LibraryJarFile.of(destDir.resolve(jarName()));
205
206        buildJar(jarResource);
207        return List.of((T) jarResource);
208    }
209}