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}