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