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.io.File;
022import java.nio.file.Path;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.List;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029import java.util.stream.Stream;
030import javax.tools.DiagnosticCollector;
031import javax.tools.JavaFileObject;
032import javax.tools.ToolProvider;
033import org.jdrupes.builder.api.BuildException;
034import org.jdrupes.builder.api.FileResource;
035import org.jdrupes.builder.api.FileTree;
036import org.jdrupes.builder.api.Project;
037import org.jdrupes.builder.api.Resource;
038import org.jdrupes.builder.api.ResourceRequest;
039import org.jdrupes.builder.api.ResourceType;
040import static org.jdrupes.builder.api.ResourceType.*;
041import org.jdrupes.builder.api.Resources;
042import org.jdrupes.builder.core.StreamCollector;
043import static org.jdrupes.builder.java.JavaTypes.*;
044
045/// The [Javadoc] generator provides the resource [JavadocDirectory],
046/// a directory that contains the generated javadoc files.
047///
048/// No attempt has been made to define types for the options of
049/// the javadoc tool. Rather, the options are passed as strings
050/// as the [ToolProvider] API suggests. There are some noteworthy
051/// exceptions for options that are directly related to resource
052/// types (files, directory trees, paths) from the builder context.
053///
054public class Javadoc extends JavaTool {
055
056    private final StreamCollector<FileTree<JavaSourceFile>> sources
057        = StreamCollector.cached();
058    private Path destination = Path.of("doc");
059    private final Resources<ClasspathElement> tagletpath;
060    private final List<String> taglets = new ArrayList<>();
061
062    /// Instantiates a new java compiler.
063    ///
064    /// @param project the project
065    ///
066    public Javadoc(Project project) {
067        super(project);
068        tagletpath = project().newResource(new ResourceType<>() {});
069    }
070
071    /// Returns the destination directory. Defaults to "`doc`".
072    ///
073    /// @return the destination
074    ///
075    public Path destination() {
076        return destination;
077    }
078
079    /// Sets the destination directory. The [Path] is resolved against
080    /// the project's build directory (see [Project#buildDirectory]).
081    ///
082    /// @param destination the new destination
083    /// @return the java compiler
084    ///
085    public Javadoc destination(Path destination) {
086        this.destination = destination;
087        return this;
088    }
089
090    /// Adds the source tree.
091    ///
092    /// @param sources the sources
093    /// @return the java compiler
094    ///
095    @SafeVarargs
096    public final Javadoc addSources(FileTree<JavaSourceFile>... sources) {
097        this.sources.add(Arrays.stream(sources));
098        return this;
099    }
100
101    /// Adds the files from the given directory matching the given pattern.
102    /// Short for
103    /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`.
104    ///
105    /// @param directory the directory
106    /// @param pattern the pattern
107    /// @return the resources collector
108    /// 
109    public final Javadoc addSources(Path directory, String pattern) {
110        addSources(
111            project().newResource(JavaSourceTreeType, directory, pattern));
112        return this;
113    }
114
115    /// Adds the sources.
116    ///
117    /// @param sources the sources
118    /// @return the java compiler
119    ///
120    public final Javadoc addSources(Stream<FileTree<JavaSourceFile>> sources) {
121        this.sources.add(sources);
122        return this;
123    }
124
125    /// Source paths.
126    ///
127    /// @return the collection
128    ///
129    private Collection<Path> sourcePaths() {
130        return sources.stream().map(Resources::stream)
131            .flatMap(Function.identity()).map(FileResource::path)
132            .collect(Collectors.toSet());
133    }
134
135    /// Adds the given elements to the taglepath.
136    ///
137    /// @param classpathElements the classpath elements
138    /// @return the javadoc
139    ///
140    public Javadoc tagletpath(Stream<ClasspathElement> classpathElements) {
141        tagletpath.addAll(classpathElements);
142        return this;
143    }
144
145    /// Adds the given taglets.
146    ///
147    /// @param taglets the taglets
148    /// @return the javadoc
149    ///
150    public Javadoc taglets(Stream<String> taglets) {
151        this.taglets.addAll(taglets.toList());
152        return this;
153    }
154
155    @Override
156    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
157        "PMD.ExceptionAsFlowControl" })
158    protected <T extends Resource> Stream<T>
159            doProvide(ResourceRequest<T> requested) {
160        if (!requested.includes(JavadocDirectoryType)
161            && !requested.includes(CleanlinessType)) {
162            return Stream.empty();
163        }
164
165        // Get destination and check if we only have to cleanup.
166        var destDir = project().buildDirectory().resolve(destination);
167        var generated = project().newResource(ClassTreeType, destDir, "**/*");
168        if (requested.includes(CleanlinessType)) {
169            generated.delete();
170            destDir.toFile().delete();
171            return Stream.empty();
172        }
173
174        // Generate
175        var javadoc = ToolProvider.getSystemDocumentationTool();
176        var diagnostics = new DiagnosticCollector<JavaFileObject>();
177        try (var fileManager
178            = javadoc.getStandardFileManager(diagnostics, null, null)) {
179            if (options().contains("-d")) {
180                new BuildException(project()
181                    + ": Specifying the destination directory with "
182                    + "options() is not allowed.");
183            }
184            List<String> allOptions = new ArrayList<>(options());
185            allOptions.addAll(List.of("-d", destDir.toString()));
186            var tagletPath = tagletPath();
187            if (!tagletPath.isEmpty()) {
188                allOptions.addAll(List.of("-tagletpath", tagletPath));
189            }
190            for (var taglet : taglets) {
191                allOptions.addAll(List.of("-taglet", taglet));
192            }
193            var sourceFiles
194                = fileManager.getJavaFileObjectsFromPaths(sourcePaths());
195            if (!javadoc.getTask(null, fileManager, diagnostics, null,
196                allOptions, sourceFiles).call()) {
197                throw new BuildException("Documentation generation failed");
198            }
199        } catch (Exception e) {
200            log.log(java.util.logging.Level.SEVERE, () -> "Project "
201                + project().name() + ": " + "Problem compiling Java: "
202                + e.getMessage());
203            throw new BuildException(e);
204        } finally {
205            logDiagnostics(diagnostics);
206        }
207        @SuppressWarnings("unchecked")
208        var result = (Stream<T>) Stream
209            .of(project().newResource(JavadocDirectoryType, destDir));
210        return result;
211    }
212
213    private String tagletPath() {
214        return tagletpath.stream().<Path> mapMulti((e, consumer) -> {
215            if (e instanceof ClassTree classTree) {
216                consumer.accept(classTree.root());
217            } else if (e instanceof JarFile jarFile) {
218                consumer.accept(jarFile.path());
219            }
220        }).map(Path::toString).collect(Collectors.joining(File.pathSeparator));
221    }
222}