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 static org.jdrupes.builder.api.ResourceRequest.*;
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/// The [Javadoc] generator provides the resource [JavadocDirectory],
047/// a directory that contains generated javadoc files.
048///
049/// No attempt has been made to define types for the options of
050/// the javadoc tool. Rather, the options are passed as strings
051/// as the [ToolProvider] API suggests. There are some noteworthy
052/// exceptions for options that are directly related to resource
053/// types (files, directory trees, paths) from the builder context.
054///
055/// By default, the generator builds the javadoc for the project passed
056/// to the constructor. In some cases, e.g. when building a common javadoc
057/// for multiple projects, this is not the desired behavior. In this case,
058/// the project(s) to generate javadoc can be set with [#projects].
059///
060/// The sources processed default to the project's [JavaSourceFile]s, i.e.
061/// the resources obtained by invoking
062/// `get(new ResourceRequest<FileTree<JavaSourceFile>>(new ResourceType<>() {}))`
063/// on the project(s). This can be overridden by setting the sources with
064/// one or several invocations of the `addSources`-methods.
065///
066public class Javadoc extends JavaTool {
067
068    private final StreamCollector<FileTree<JavaSourceFile>> sources
069        = StreamCollector.cached();
070    private StreamCollector<Project> projects = StreamCollector.cached();
071    private Path destination = Path.of("doc");
072    private final Resources<ClasspathElement> tagletpath;
073    private final List<String> taglets = new ArrayList<>();
074
075    /// Instantiates a new java compiler.
076    ///
077    /// @param project the project
078    ///
079    public Javadoc(Project project) {
080        super(project);
081        projects.add(project);
082        tagletpath = project().newResource(new ResourceType<>() {});
083    }
084
085    /// Sets the projects to generate javadoc for.
086    ///
087    /// @param projects the projects
088    /// @return the javadoc
089    ///
090    public Javadoc projects(Stream<Project> projects) {
091        this.projects = StreamCollector.cached();
092        this.projects.add(projects);
093        return this;
094    }
095
096    /// Returns the destination directory. Defaults to "`doc`".
097    ///
098    /// @return the destination
099    ///
100    public Path destination() {
101        return destination;
102    }
103
104    /// Sets the destination directory. The [Path] is resolved against
105    /// the project's build directory (see [Project#buildDirectory]).
106    ///
107    /// @param destination the new destination
108    /// @return the java compiler
109    ///
110    public Javadoc destination(Path destination) {
111        this.destination = destination;
112        return this;
113    }
114
115    /// Adds the source tree.
116    ///
117    /// @param sources the sources
118    /// @return the java compiler
119    ///
120    @SafeVarargs
121    public final Javadoc addSources(FileTree<JavaSourceFile>... sources) {
122        this.sources.add(Arrays.stream(sources));
123        return this;
124    }
125
126    /// Adds the files from the given directory matching the given pattern.
127    /// Short for
128    /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`.
129    ///
130    /// @param directory the directory
131    /// @param pattern the pattern
132    /// @return the resources collector
133    /// 
134    public final Javadoc addSources(Path directory, String pattern) {
135        addSources(
136            project().newResource(JavaSourceTreeType, directory, pattern));
137        return this;
138    }
139
140    /// Adds the sources.
141    ///
142    /// @param sources the sources
143    /// @return the java compiler
144    ///
145    public final Javadoc addSources(Stream<FileTree<JavaSourceFile>> sources) {
146        this.sources.add(sources);
147        return this;
148    }
149
150    /// Source paths.
151    ///
152    /// @return the collection
153    ///
154    private Collection<Path> sourcePaths(
155            Stream<FileTree<JavaSourceFile>> sources) {
156        return sources.map(Resources::stream)
157            .flatMap(Function.identity()).map(FileResource::path)
158            .collect(Collectors.toSet());
159    }
160
161    /// Adds the given elements to the taglepath.
162    ///
163    /// @param classpathElements the classpath elements
164    /// @return the javadoc
165    ///
166    public Javadoc tagletpath(Stream<ClasspathElement> classpathElements) {
167        tagletpath.addAll(classpathElements);
168        return this;
169    }
170
171    /// Adds the given taglets.
172    ///
173    /// @param taglets the taglets
174    /// @return the javadoc
175    ///
176    public Javadoc taglets(Stream<String> taglets) {
177        this.taglets.addAll(taglets.toList());
178        return this;
179    }
180
181    @Override
182    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
183        "PMD.ExceptionAsFlowControl" })
184    protected <T extends Resource> Stream<T>
185            doProvide(ResourceRequest<T> requested) {
186        if (!requested.collects(JavadocDirectoryType)
187            && !requested.collects(CleanlinessType)) {
188            return Stream.empty();
189        }
190
191        // Get destination and check if we only have to cleanup.
192        var destDir = project().buildDirectory().resolve(destination);
193        var generated = project().newResource(ClassTreeType, destDir, "**/*");
194        if (requested.collects(CleanlinessType)) {
195            generated.delete();
196            destDir.toFile().delete();
197            return Stream.empty();
198        }
199
200        // Generate
201        var javadoc = ToolProvider.getSystemDocumentationTool();
202        var diagnostics = new DiagnosticCollector<JavaFileObject>();
203        try (var fileManager
204            = javadoc.getStandardFileManager(diagnostics, null, null)) {
205            List<String> allOptions = evaluateOptions(destDir);
206            log.finest(() -> "Javadoc options: " + allOptions);
207            var sourcePaths = sourcePaths(sources.stream());
208            if (sourcePaths.isEmpty()) {
209                sourcePaths = sourcePaths(projects.stream().flatMap(p -> p
210                    .get(new ResourceRequest<FileTree<JavaSourceFile>>(
211                        new ResourceType<>() {}))));
212            }
213            var finalSourcePaths = sourcePaths;
214            log.finest(() -> "Javadoc sources: " + finalSourcePaths);
215            var sourceFiles
216                = fileManager.getJavaFileObjectsFromPaths(sourcePaths);
217            if (!javadoc.getTask(null, fileManager, diagnostics, null,
218                allOptions, sourceFiles).call()) {
219                throw new BuildException("Documentation generation failed");
220            }
221        } catch (Exception e) {
222            log.log(java.util.logging.Level.SEVERE, () -> "Project "
223                + project().name() + ": " + "Problem generating Javadoc: "
224                + e.getMessage());
225            throw new BuildException(e);
226        } finally {
227            logDiagnostics(diagnostics);
228        }
229        @SuppressWarnings("unchecked")
230        var result = (Stream<T>) Stream
231            .of(project().newResource(JavadocDirectoryType, destDir));
232        return result;
233    }
234
235    private List<String> evaluateOptions(Path destDir) {
236        if (options().contains("-d")) {
237            new BuildException(project()
238                + ": Specifying the destination directory with "
239                + "options() is not allowed.");
240        }
241        List<String> allOptions = new ArrayList<>(options());
242        allOptions.addAll(List.of("-d", destDir.toString()));
243
244        // Handle classpath
245        var cpResources = newResource(ClasspathType).addAll(projects.stream()
246            .flatMap(p -> p.provided(requestFor(ClasspathElement.class))));
247        log.finest(() -> "Generating in " + project() + " with classpath "
248            + cpResources.stream().map(Resource::toString).toList());
249        if (!cpResources.isEmpty()) {
250            var classpath = cpResources.stream().map(e -> e.toPath().toString())
251                .collect(Collectors.joining(File.pathSeparator));
252            allOptions.addAll(List.of("-cp", classpath));
253        }
254
255        // Handle taglets
256        var tagletPath = tagletPath();
257        if (!tagletPath.isEmpty()) {
258            allOptions.addAll(List.of("-tagletpath", tagletPath));
259        }
260        for (var taglet : taglets) {
261            allOptions.addAll(List.of("-taglet", taglet));
262        }
263        return allOptions;
264    }
265
266    private String tagletPath() {
267        return tagletpath.stream().<Path> mapMulti((e, consumer) -> {
268            if (e instanceof ClassTree classTree) {
269                consumer.accept(classTree.root());
270            } else if (e instanceof JarFile jarFile) {
271                consumer.accept(jarFile.path());
272            }
273        }).map(Path::toString).collect(Collectors.joining(File.pathSeparator));
274    }
275}