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