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 and module-path for the Javadoc tool invocation are 067/// assembled by requesting the [CodeContribution]s with intents 068/// [Consume][Intent#Consume], [Reveal][Intent#Reveal] and 069/// [Expose][Intent#Expose] from the configured 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/// 078/// ## JPMS support 079/// 080/// When the sources contain a `module-info.java`, or when the module mode 081/// is set to `ModuleMode.MODULE`, generation uses the module-path 082/// (`--module-path`) for code contributions that contain a module descriptor. 083/// Non-modular code contributions remain on the classpath (`-cp`). In 084/// `ModuleMode.AUTO` mode (the default), module compilation is activated 085/// automatically when a `module-info.java` is found among the sources. 086/// 087public class Javadoc extends JavaTool { 088 089 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 090 private final StreamCollector<FileTree<JavaSourceFile>> sources 091 = StreamCollector.cached(); 092 private StreamCollector<Project> projects = StreamCollector.cached(); 093 private Path destination = Path.of("doc"); 094 private final StreamCollector<CodeContribution> tagletpath 095 = StreamCollector.cached(); 096 private final List<String> taglets = new ArrayList<>(); 097 098 /// Instantiates a new java compiler. 099 /// 100 /// @param project the project 101 /// 102 public Javadoc(Project project) { 103 super(project); 104 projects.add(project); 105 } 106 107 /// Sets the projects to generate javadoc for. 108 /// 109 /// @param projects the projects 110 /// @return the javadoc 111 /// 112 public Javadoc projects(Stream<Project> projects) { 113 this.projects = StreamCollector.cached(); 114 this.projects.add(projects); 115 return this; 116 } 117 118 /// Returns the destination directory. Defaults to "`doc`". 119 /// 120 /// @return the destination 121 /// 122 public Path destination() { 123 return destination; 124 } 125 126 /// Sets the destination directory. The [Path] is resolved against 127 /// the project's build directory (see [Project#buildDirectory]). 128 /// 129 /// @param destination the new destination 130 /// @return the javadoc generator 131 /// 132 public Javadoc destination(Path destination) { 133 this.destination = destination; 134 return this; 135 } 136 137 /// Adds the source tree. 138 /// 139 /// @param sources the sources 140 /// @return the javadoc generator 141 /// 142 @SafeVarargs 143 public final Javadoc addSources(FileTree<JavaSourceFile>... sources) { 144 this.sources.add(Arrays.stream(sources)); 145 return this; 146 } 147 148 /// Adds the files from the given directory matching the given pattern. 149 /// Short for 150 /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`. 151 /// 152 /// @param directory the directory 153 /// @param pattern the pattern 154 /// @return the javadoc generator 155 /// 156 public final Javadoc addSources(Path directory, String pattern) { 157 addSources(FileTree.of( 158 project(), directory, JavaSourceFile.class, pattern)); 159 return this; 160 } 161 162 /// Adds the sources. 163 /// 164 /// @param sources the sources 165 /// @return the java compiler 166 /// 167 public final Javadoc addSources(Stream<FileTree<JavaSourceFile>> sources) { 168 this.sources.add(sources); 169 return this; 170 } 171 172 /// Source paths. 173 /// 174 /// @return the collection 175 /// 176 private Collection<Path> sourcePaths( 177 Stream<FileTree<JavaSourceFile>> sources) { 178 return sources.map(Resources::stream) 179 .flatMap(Function.identity()).map(FileResource::path) 180 .collect(Collectors.toSet()); 181 } 182 183 @Override 184 protected boolean hasModuleInfo() { 185 return sources.stream().flatMap(Resources::stream) 186 .anyMatch(f -> f.path().endsWith("module-info.java")); 187 } 188 189 /// Adds the given classpath elements to the tagletpath. The classpath 190 /// elements can be classpath elements or modules. 191 /// 192 /// @param classpathElements the classpath elements 193 /// @return the javadoc 194 /// 195 public Javadoc 196 tagletpath(Stream<? extends CodeContribution> classpathElements) { 197 tagletpath.add(classpathElements); 198 return this; 199 } 200 201 /// Adds the given taglets. 202 /// 203 /// @param taglets the taglets 204 /// @return the javadoc 205 /// 206 public Javadoc taglets(Stream<String> taglets) { 207 this.taglets.addAll(taglets.toList()); 208 return this; 209 } 210 211 @Override 212 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 213 "PMD.ExceptionAsFlowControl" }) 214 protected <T extends Resource> Collection<T> 215 doProvide(ResourceRequest<T> requested) { 216 if (!requested.accepts(JavadocDirectoryType) 217 && !requested.accepts(CleanlinessType)) { 218 return Collections.emptyList(); 219 } 220 221 // Get destination and check if we only have to cleanup. 222 var destDir = project().buildDirectory().resolve(destination); 223 var generated = ClassTree.of(project(), destDir); 224 if (requested.accepts(CleanlinessType)) { 225 generated.cleanup(); 226 destDir.toFile().delete(); 227 return Collections.emptyList(); 228 } 229 230 // Always evaluate for most special type 231 if (!requested.type().equals(JavadocDirectoryType)) { 232 @SuppressWarnings("unchecked") 233 var result 234 = (Collection<T>) resources(of(JavadocDirectoryType)).toList(); 235 return result; 236 } 237 238 // Generate 239 var javadoc = ToolProvider.getSystemDocumentationTool(); 240 var diagnostics = new DiagnosticCollector<JavaFileObject>(); 241 try (var fileManager 242 = javadoc.getStandardFileManager(diagnostics, null, null)) { 243 List<String> allOptions = evaluateOptions(destDir); 244 logger.atFinest().log("Javadoc options: %s", allOptions); 245 var sourcePaths = sourcePaths(sources.stream()); 246 if (sourcePaths.isEmpty()) { 247 sourcePaths = sourcePaths(projects.stream().flatMap(p -> p 248 .resources(of(JavaSourceTreeType).using(Supply, Expose)))); 249 } 250 var finalSourcePaths = sourcePaths; 251 logger.atFinest().log("Javadoc sources: %s", finalSourcePaths); 252 var sourceFiles 253 = fileManager.getJavaFileObjectsFromPaths(sourcePaths); 254 if (!javadoc.getTask(null, fileManager, diagnostics, null, 255 allOptions, sourceFiles).call()) { 256 throw new UnavailableException().from(this); 257 } 258 } catch (Exception e) { 259 logger.atSevere().withCause(e).log( 260 "Project %s: Cannot generate Javadoc: %s", 261 project().name(), e.getMessage()); 262 throw new BuildException().from(this).cause(e); 263 } finally { 264 logDiagnostics(diagnostics); 265 } 266 @SuppressWarnings("unchecked") 267 var result = (Collection<T>) List.of( 268 JavadocDirectory.of(project(), destDir)); 269 return result; 270 } 271 272 private List<String> evaluateOptions(Path destDir) { 273 if (options().contains("-d")) { 274 new ConfigurationException().from(this).message("Specifying the" 275 + " destination directory with options() is not allowed."); 276 } 277 List<String> allOptions = new ArrayList<>(options()); 278 allOptions.addAll(List.of("-d", destDir.toString())); 279 280 // Handle classpath and module-path 281 var ccResources = Resources.of(CodeContributionsType).addAll( 282 projects.stream().flatMap(p -> p.resources( 283 of(CodeContributionType).using(Consume, Reveal, Expose)))); 284 var useModulePath = effectiveModuleMode() == ModuleMode.MODULE; 285 var cpe = ccResources.stream() 286 .filter(cc -> !useModulePath || !cc.isModular()).toList(); 287 var mpe = ccResources.stream() 288 .filter(cc -> useModulePath && cc.isModular()).toList(); 289 logger.atFinest().log( 290 "Generating in %s with classpath %s and module-path %s", project(), 291 lazy(() -> cpe.stream().map(Resource::toString).toList()), 292 lazy(() -> mpe.stream().map(Resource::toString).toList())); 293 if (!cpe.isEmpty()) { 294 var classpath = cpe.stream().map(e -> e.toPath().toString()) 295 .collect(Collectors.joining(File.pathSeparator)); 296 allOptions.addAll(List.of("-cp", classpath)); 297 } 298 if (!mpe.isEmpty()) { 299 var modulePath = mpe.stream().map(e -> e.toPath().toString()) 300 .collect(Collectors.joining(File.pathSeparator)); 301 allOptions.addAll(List.of("--module-path", modulePath)); 302 } 303 304 // Handle taglets 305 var tagletPath = tagletPath(); 306 if (!tagletPath.isEmpty()) { 307 allOptions.addAll(List.of("-tagletpath", tagletPath)); 308 } 309 for (var taglet : taglets) { 310 allOptions.addAll(List.of("-taglet", taglet)); 311 } 312 return allOptions; 313 } 314 315 private String tagletPath() { 316 return tagletpath.stream().<Path> mapMulti((e, consumer) -> { 317 if (e instanceof ClassTree classTree) { 318 consumer.accept(classTree.root()); 319 } else if (e instanceof JarFile jarFile) { 320 consumer.accept(jarFile.path()); 321 } 322 }).map(Path::toString).collect(Collectors.joining(File.pathSeparator)); 323 } 324}