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 java.io.File; 022import java.nio.file.Path; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.List; 027import java.util.function.Function; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030import javax.tools.DiagnosticCollector; 031import javax.tools.JavaFileObject; 032import javax.tools.ToolProvider; 033import org.jdrupes.builder.api.BuildException; 034import org.jdrupes.builder.api.FileResource; 035import org.jdrupes.builder.api.FileTree; 036import org.jdrupes.builder.api.Project; 037import org.jdrupes.builder.api.Resource; 038import org.jdrupes.builder.api.ResourceRequest; 039import static org.jdrupes.builder.api.ResourceRequest.*; 040import org.jdrupes.builder.api.ResourceType; 041import static org.jdrupes.builder.api.ResourceType.*; 042import org.jdrupes.builder.api.Resources; 043import org.jdrupes.builder.core.StreamCollector; 044import static org.jdrupes.builder.java.JavaTypes.*; 045 046/// The [Javadoc] generator provides the resource [JavadocDirectory], 047/// a directory that contains generated javadoc files. 048/// 049/// No attempt has been made to define types for the options of 050/// the javadoc tool. Rather, the options are passed as strings 051/// as the [ToolProvider] API suggests. There are some noteworthy 052/// exceptions for options that are directly related to resource 053/// types (files, directory trees, paths) from the builder context. 054/// 055/// By default, the generator builds the javadoc for the project passed 056/// to the constructor. In some cases, e.g. when building a common javadoc 057/// for multiple projects, this is not the desired behavior. In this case, 058/// the project(s) to generate javadoc can be set with [#projects]. 059/// 060/// The sources processed default to the project's [JavaSourceFile]s, i.e. 061/// the resources obtained by invoking 062/// `get(new ResourceRequest<FileTree<JavaSourceFile>>(new ResourceType<>() {}))` 063/// on the project(s). This can be overridden by setting the sources with 064/// one or several invocations of the `addSources`-methods. 065/// 066public class Javadoc extends JavaTool { 067 068 private final StreamCollector<FileTree<JavaSourceFile>> sources 069 = StreamCollector.cached(); 070 private StreamCollector<Project> projects = StreamCollector.cached(); 071 private Path destination = Path.of("doc"); 072 private final Resources<ClasspathElement> tagletpath; 073 private final List<String> taglets = new ArrayList<>(); 074 075 /// Instantiates a new java compiler. 076 /// 077 /// @param project the project 078 /// 079 public Javadoc(Project project) { 080 super(project); 081 projects.add(project); 082 tagletpath = project().newResource(new ResourceType<>() {}); 083 } 084 085 /// Sets the projects to generate javadoc for. 086 /// 087 /// @param projects the projects 088 /// @return the javadoc 089 /// 090 public Javadoc projects(Stream<Project> projects) { 091 this.projects = StreamCollector.cached(); 092 this.projects.add(projects); 093 return this; 094 } 095 096 /// Returns the destination directory. Defaults to "`doc`". 097 /// 098 /// @return the destination 099 /// 100 public Path destination() { 101 return destination; 102 } 103 104 /// Sets the destination directory. The [Path] is resolved against 105 /// the project's build directory (see [Project#buildDirectory]). 106 /// 107 /// @param destination the new destination 108 /// @return the java compiler 109 /// 110 public Javadoc destination(Path destination) { 111 this.destination = destination; 112 return this; 113 } 114 115 /// Adds the source tree. 116 /// 117 /// @param sources the sources 118 /// @return the java compiler 119 /// 120 @SafeVarargs 121 public final Javadoc addSources(FileTree<JavaSourceFile>... sources) { 122 this.sources.add(Arrays.stream(sources)); 123 return this; 124 } 125 126 /// Adds the files from the given directory matching the given pattern. 127 /// Short for 128 /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`. 129 /// 130 /// @param directory the directory 131 /// @param pattern the pattern 132 /// @return the resources collector 133 /// 134 public final Javadoc addSources(Path directory, String pattern) { 135 addSources( 136 project().newResource(JavaSourceTreeType, directory, pattern)); 137 return this; 138 } 139 140 /// Adds the sources. 141 /// 142 /// @param sources the sources 143 /// @return the java compiler 144 /// 145 public final Javadoc addSources(Stream<FileTree<JavaSourceFile>> sources) { 146 this.sources.add(sources); 147 return this; 148 } 149 150 /// Source paths. 151 /// 152 /// @return the collection 153 /// 154 private Collection<Path> sourcePaths( 155 Stream<FileTree<JavaSourceFile>> sources) { 156 return sources.map(Resources::stream) 157 .flatMap(Function.identity()).map(FileResource::path) 158 .collect(Collectors.toSet()); 159 } 160 161 /// Adds the given elements to the taglepath. 162 /// 163 /// @param classpathElements the classpath elements 164 /// @return the javadoc 165 /// 166 public Javadoc tagletpath(Stream<ClasspathElement> classpathElements) { 167 tagletpath.addAll(classpathElements); 168 return this; 169 } 170 171 /// Adds the given taglets. 172 /// 173 /// @param taglets the taglets 174 /// @return the javadoc 175 /// 176 public Javadoc taglets(Stream<String> taglets) { 177 this.taglets.addAll(taglets.toList()); 178 return this; 179 } 180 181 @Override 182 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 183 "PMD.ExceptionAsFlowControl" }) 184 protected <T extends Resource> Stream<T> 185 doProvide(ResourceRequest<T> requested) { 186 if (!requested.collects(JavadocDirectoryType) 187 && !requested.collects(CleanlinessType)) { 188 return Stream.empty(); 189 } 190 191 // Get destination and check if we only have to cleanup. 192 var destDir = project().buildDirectory().resolve(destination); 193 var generated = project().newResource(ClassTreeType, destDir, "**/*"); 194 if (requested.collects(CleanlinessType)) { 195 generated.delete(); 196 destDir.toFile().delete(); 197 return Stream.empty(); 198 } 199 200 // Generate 201 var javadoc = ToolProvider.getSystemDocumentationTool(); 202 var diagnostics = new DiagnosticCollector<JavaFileObject>(); 203 try (var fileManager 204 = javadoc.getStandardFileManager(diagnostics, null, null)) { 205 List<String> allOptions = evaluateOptions(destDir); 206 log.finest(() -> "Javadoc options: " + allOptions); 207 var sourcePaths = sourcePaths(sources.stream()); 208 if (sourcePaths.isEmpty()) { 209 sourcePaths = sourcePaths(projects.stream().flatMap(p -> p 210 .get(new ResourceRequest<FileTree<JavaSourceFile>>( 211 new ResourceType<>() {})))); 212 } 213 var finalSourcePaths = sourcePaths; 214 log.finest(() -> "Javadoc sources: " + finalSourcePaths); 215 var sourceFiles 216 = fileManager.getJavaFileObjectsFromPaths(sourcePaths); 217 if (!javadoc.getTask(null, fileManager, diagnostics, null, 218 allOptions, sourceFiles).call()) { 219 throw new BuildException("Documentation generation failed"); 220 } 221 } catch (Exception e) { 222 log.log(java.util.logging.Level.SEVERE, () -> "Project " 223 + project().name() + ": " + "Problem generating Javadoc: " 224 + e.getMessage()); 225 throw new BuildException(e); 226 } finally { 227 logDiagnostics(diagnostics); 228 } 229 @SuppressWarnings("unchecked") 230 var result = (Stream<T>) Stream 231 .of(project().newResource(JavadocDirectoryType, destDir)); 232 return result; 233 } 234 235 private List<String> evaluateOptions(Path destDir) { 236 if (options().contains("-d")) { 237 new BuildException(project() 238 + ": Specifying the destination directory with " 239 + "options() is not allowed."); 240 } 241 List<String> allOptions = new ArrayList<>(options()); 242 allOptions.addAll(List.of("-d", destDir.toString())); 243 244 // Handle classpath 245 var cpResources = newResource(ClasspathType).addAll(projects.stream() 246 .flatMap(p -> p.provided(requestFor(ClasspathElement.class)))); 247 log.finest(() -> "Generating in " + project() + " with classpath " 248 + cpResources.stream().map(Resource::toString).toList()); 249 if (!cpResources.isEmpty()) { 250 var classpath = cpResources.stream().map(e -> e.toPath().toString()) 251 .collect(Collectors.joining(File.pathSeparator)); 252 allOptions.addAll(List.of("-cp", classpath)); 253 } 254 255 // Handle taglets 256 var tagletPath = tagletPath(); 257 if (!tagletPath.isEmpty()) { 258 allOptions.addAll(List.of("-tagletpath", tagletPath)); 259 } 260 for (var taglet : taglets) { 261 allOptions.addAll(List.of("-taglet", taglet)); 262 } 263 return allOptions; 264 } 265 266 private String tagletPath() { 267 return tagletpath.stream().<Path> mapMulti((e, consumer) -> { 268 if (e instanceof ClassTree classTree) { 269 consumer.accept(classTree.root()); 270 } else if (e instanceof JarFile jarFile) { 271 consumer.accept(jarFile.path()); 272 } 273 }).map(Path::toString).collect(Collectors.joining(File.pathSeparator)); 274 } 275}