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 static org.jdrupes.builder.api.Intent.*;
041import org.jdrupes.builder.api.Project;
042import org.jdrupes.builder.api.Resource;
043import org.jdrupes.builder.api.ResourceProvider;
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 (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 StreamCollector<ClasspathElement> tagletpath
081        = StreamCollector.cached();
082    private final List<String> taglets = new ArrayList<>();
083
084    /// Instantiates a new java compiler.
085    ///
086    /// @param project the project
087    ///
088    public Javadoc(Project project) {
089        super(project);
090        projects.add(project);
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.add(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> Collection<T>
193            doProvide(ResourceRequest<T> requested) {
194        if (!requested.accepts(JavadocDirectoryType)
195            && !requested.accepts(CleanlinessType)) {
196            return Collections.emptyList();
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 Collections.emptyList();
206        }
207
208        // Always evaluate for most special type
209        if (!requested.type().equals(JavadocDirectoryType)) {
210            @SuppressWarnings("unchecked")
211            var result
212                = (Collection<T>) resources(of(JavadocDirectoryType)).toList();
213            return result;
214        }
215
216        // Generate
217        var javadoc = ToolProvider.getSystemDocumentationTool();
218        var diagnostics = new DiagnosticCollector<JavaFileObject>();
219        try (var fileManager
220            = javadoc.getStandardFileManager(diagnostics, null, null)) {
221            List<String> allOptions = evaluateOptions(destDir);
222            logger.atFinest().log("Javadoc options: %s", allOptions);
223            var sourcePaths = sourcePaths(sources.stream());
224            if (sourcePaths.isEmpty()) {
225                sourcePaths = sourcePaths(projects.stream().flatMap(p -> p
226                    .resources(of(JavaSourceTreeType).using(Supply, Expose))));
227            }
228            var finalSourcePaths = sourcePaths;
229            logger.atFinest().log("Javadoc sources: %s", finalSourcePaths);
230            var sourceFiles
231                = fileManager.getJavaFileObjectsFromPaths(sourcePaths);
232            if (!javadoc.getTask(null, fileManager, diagnostics, null,
233                allOptions, sourceFiles).call()) {
234                throw new UnavailableException().from(this);
235            }
236        } catch (Exception e) {
237            logger.atSevere().withCause(e).log(
238                "Project %s: Cannot generate Javadoc: %s",
239                project().name(), e.getMessage());
240            throw new BuildException().from(this).cause(e);
241        } finally {
242            logDiagnostics(diagnostics);
243        }
244        @SuppressWarnings("unchecked")
245        var result = (Collection<T>) List.of(
246            JavadocDirectory.of(project(), destDir));
247        return result;
248    }
249
250    private List<String> evaluateOptions(Path destDir) {
251        if (options().contains("-d")) {
252            new ConfigurationException().from(this).message("Specifying the"
253                + " destination directory with options() is not allowed.");
254        }
255        List<String> allOptions = new ArrayList<>(options());
256        allOptions.addAll(List.of("-d", destDir.toString()));
257
258        // Handle classpath
259        var cpResources = Resources.of(ClasspathType).addAll(projects.stream()
260            .flatMap(p -> p.resources(of(ClasspathElementType)
261                .using(Consume, Reveal, Expose))));
262        logger.atFinest().log("Generating in %s with classpath %s", project(),
263            lazy(() -> cpResources.stream().map(Resource::toString).toList()));
264        if (!cpResources.isEmpty()) {
265            var classpath = cpResources.stream().map(e -> e.toPath().toString())
266                .collect(Collectors.joining(File.pathSeparator));
267            allOptions.addAll(List.of("-cp", classpath));
268        }
269
270        // Handle taglets
271        var tagletPath = tagletPath();
272        if (!tagletPath.isEmpty()) {
273            allOptions.addAll(List.of("-tagletpath", tagletPath));
274        }
275        for (var taglet : taglets) {
276            allOptions.addAll(List.of("-taglet", taglet));
277        }
278        return allOptions;
279    }
280
281    private String tagletPath() {
282        return tagletpath.stream().<Path> mapMulti((e, consumer) -> {
283            if (e instanceof ClassTree classTree) {
284                consumer.accept(classTree.root());
285            } else if (e instanceof JarFile jarFile) {
286                consumer.accept(jarFile.path());
287            }
288        }).map(Path::toString).collect(Collectors.joining(File.pathSeparator));
289    }
290}