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