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