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}