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}