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 implements ResourceRetriever {
062
063    private final StreamCollector<ResourceProvider> providers
064        = StreamCollector.cached();
065    private String mainClass;
066
067    /// Instantiates a new library generator.
068    ///
069    /// @param project the project
070    ///
071    public LibraryGenerator(Project project) {
072        super(project, LibraryJarFileType);
073    }
074
075    /// Returns the main class.
076    ///
077    /// @return the main class
078    ///
079    public String mainClass() {
080        return mainClass;
081    }
082
083    /// Sets the main class.
084    ///
085    /// @param mainClass the new main class
086    /// @return the jar generator for method chaining
087    ///
088    public LibraryGenerator mainClass(String mainClass) {
089        this.mainClass = mainClass;
090        return this;
091    }
092
093    /// Additionally uses the given providers for obtaining contents for the
094    /// jar.
095    ///
096    /// @param providers the providers
097    /// @return the jar generator
098    ///
099    @Override
100    public LibraryGenerator from(ResourceProvider... providers) {
101        from(Stream.of(providers));
102        return this;
103    }
104
105    /// Additionally uses the given providers for obtaining contents for the
106    /// jar.
107    ///
108    /// @param providers the providers
109    /// @return the jar generator
110    ///
111    @Override
112    public LibraryGenerator from(Stream<ResourceProvider> providers) {
113        this.providers.add(providers.filter(p -> !p.equals(this)));
114        return this;
115    }
116
117    /// return the cached providers.
118    ///
119    /// @return the cached stream
120    ///
121    protected StreamCollector<ResourceProvider> providers() {
122        return providers;
123    }
124
125    @Override
126    protected void collectContents(Map<Path, Resources<IOResource>> contents) {
127        super.collectContents(contents);
128        // Add main class if defined
129        if (mainClass() != null) {
130            attributes(Map.entry(Attributes.Name.MAIN_CLASS, mainClass()));
131        }
132        collectFromProviders(contents);
133    }
134
135    /// Collects the contents from the providers. This implementation
136    /// requests [ClassTree]s and [JavaResourceTree]s.
137    ///
138    /// @param contents the contents
139    ///
140    protected void
141            collectFromProviders(Map<Path, Resources<IOResource>> contents) {
142        project().getFrom(providers().stream(),
143            new ResourceRequest<ClassTree>(new ResourceType<>() {}))
144            .parallel().forEach(t -> collect(contents, t));
145        project().getFrom(providers().stream(),
146            new ResourceRequest<JavaResourceTree>(new ResourceType<>() {}))
147            .parallel().forEach(t -> collect(contents, t));
148    }
149
150    @Override
151    @SuppressWarnings({ "PMD.CollapsibleIfStatements", "unchecked" })
152    protected <T extends Resource> Stream<T>
153            doProvide(ResourceRequest<T> requested) {
154        if (!requested.includes(LibraryJarFileType)
155            && !requested.includes(CleanlinessType)) {
156            return Stream.empty();
157        }
158
159        // Make sure mainClass is set for app jar
160        if (AppJarFileType.isAssignableFrom(requested.type().containedType())
161            && mainClass() == null) {
162            throw new BuildException("Main class must be set for "
163                + name() + " in " + project());
164        }
165
166        // Prepare jar file
167        var destDir = destination();
168        if (!destDir.toFile().exists()) {
169            if (!destDir.toFile().mkdirs()) {
170                throw new BuildException("Cannot create directory " + destDir);
171            }
172        }
173        var jarResource
174            = AppJarFileType.isAssignableFrom(requested.type().containedType())
175                ? project().newResource(AppJarFileType,
176                    destDir.resolve(jarName()))
177                : project().newResource(LibraryJarFileType,
178                    destDir.resolve(jarName()));
179
180        // Maybe only delete
181        if (requested.includes(CleanlinessType)) {
182            jarResource.delete();
183            return Stream.empty();
184        }
185
186        buildJar(jarResource);
187        return Stream.of((T) jarResource);
188    }
189}