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 org.jdrupes.builder.api.ResourceType; 040import static org.jdrupes.builder.api.ResourceType.*; 041import org.jdrupes.builder.api.Resources; 042import org.jdrupes.builder.core.StreamCollector; 043import static org.jdrupes.builder.java.JavaTypes.*; 044 045/// The [Javadoc] generator provides the resource [JavadocDirectory], 046/// a directory that contains the generated javadoc files. 047/// 048/// No attempt has been made to define types for the options of 049/// the javadoc tool. Rather, the options are passed as strings 050/// as the [ToolProvider] API suggests. There are some noteworthy 051/// exceptions for options that are directly related to resource 052/// types (files, directory trees, paths) from the builder context. 053/// 054public class Javadoc extends JavaTool { 055 056 private final StreamCollector<FileTree<JavaSourceFile>> sources 057 = StreamCollector.cached(); 058 private Path destination = Path.of("doc"); 059 private final Resources<ClasspathElement> tagletpath; 060 private final List<String> taglets = new ArrayList<>(); 061 062 /// Instantiates a new java compiler. 063 /// 064 /// @param project the project 065 /// 066 public Javadoc(Project project) { 067 super(project); 068 tagletpath = project().newResource(new ResourceType<>() {}); 069 } 070 071 /// Returns the destination directory. Defaults to "`doc`". 072 /// 073 /// @return the destination 074 /// 075 public Path destination() { 076 return destination; 077 } 078 079 /// Sets the destination directory. The [Path] is resolved against 080 /// the project's build directory (see [Project#buildDirectory]). 081 /// 082 /// @param destination the new destination 083 /// @return the java compiler 084 /// 085 public Javadoc destination(Path destination) { 086 this.destination = destination; 087 return this; 088 } 089 090 /// Adds the source tree. 091 /// 092 /// @param sources the sources 093 /// @return the java compiler 094 /// 095 @SafeVarargs 096 public final Javadoc addSources(FileTree<JavaSourceFile>... sources) { 097 this.sources.add(Arrays.stream(sources)); 098 return this; 099 } 100 101 /// Adds the files from the given directory matching the given pattern. 102 /// Short for 103 /// `addSources(project().newFileTree(directory, pattern, JavaSourceFile.class))`. 104 /// 105 /// @param directory the directory 106 /// @param pattern the pattern 107 /// @return the resources collector 108 /// 109 public final Javadoc addSources(Path directory, String pattern) { 110 addSources( 111 project().newResource(JavaSourceTreeType, directory, pattern)); 112 return this; 113 } 114 115 /// Adds the sources. 116 /// 117 /// @param sources the sources 118 /// @return the java compiler 119 /// 120 public final Javadoc addSources(Stream<FileTree<JavaSourceFile>> sources) { 121 this.sources.add(sources); 122 return this; 123 } 124 125 /// Source paths. 126 /// 127 /// @return the collection 128 /// 129 private Collection<Path> sourcePaths() { 130 return sources.stream().map(Resources::stream) 131 .flatMap(Function.identity()).map(FileResource::path) 132 .collect(Collectors.toSet()); 133 } 134 135 /// Adds the given elements to the taglepath. 136 /// 137 /// @param classpathElements the classpath elements 138 /// @return the javadoc 139 /// 140 public Javadoc tagletpath(Stream<ClasspathElement> classpathElements) { 141 tagletpath.addAll(classpathElements); 142 return this; 143 } 144 145 /// Adds the given taglets. 146 /// 147 /// @param taglets the taglets 148 /// @return the javadoc 149 /// 150 public Javadoc taglets(Stream<String> taglets) { 151 this.taglets.addAll(taglets.toList()); 152 return this; 153 } 154 155 @Override 156 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 157 "PMD.ExceptionAsFlowControl" }) 158 protected <T extends Resource> Stream<T> 159 doProvide(ResourceRequest<T> requested) { 160 if (!requested.includes(JavadocDirectoryType) 161 && !requested.includes(CleanlinessType)) { 162 return Stream.empty(); 163 } 164 165 // Get destination and check if we only have to cleanup. 166 var destDir = project().buildDirectory().resolve(destination); 167 var generated = project().newResource(ClassTreeType, destDir, "**/*"); 168 if (requested.includes(CleanlinessType)) { 169 generated.delete(); 170 destDir.toFile().delete(); 171 return Stream.empty(); 172 } 173 174 // Generate 175 var javadoc = ToolProvider.getSystemDocumentationTool(); 176 var diagnostics = new DiagnosticCollector<JavaFileObject>(); 177 try (var fileManager 178 = javadoc.getStandardFileManager(diagnostics, null, null)) { 179 if (options().contains("-d")) { 180 new BuildException(project() 181 + ": Specifying the destination directory with " 182 + "options() is not allowed."); 183 } 184 List<String> allOptions = new ArrayList<>(options()); 185 allOptions.addAll(List.of("-d", destDir.toString())); 186 var tagletPath = tagletPath(); 187 if (!tagletPath.isEmpty()) { 188 allOptions.addAll(List.of("-tagletpath", tagletPath)); 189 } 190 for (var taglet : taglets) { 191 allOptions.addAll(List.of("-taglet", taglet)); 192 } 193 var sourceFiles 194 = fileManager.getJavaFileObjectsFromPaths(sourcePaths()); 195 if (!javadoc.getTask(null, fileManager, diagnostics, null, 196 allOptions, sourceFiles).call()) { 197 throw new BuildException("Documentation generation failed"); 198 } 199 } catch (Exception e) { 200 log.log(java.util.logging.Level.SEVERE, () -> "Project " 201 + project().name() + ": " + "Problem compiling Java: " 202 + e.getMessage()); 203 throw new BuildException(e); 204 } finally { 205 logDiagnostics(diagnostics); 206 } 207 @SuppressWarnings("unchecked") 208 var result = (Stream<T>) Stream 209 .of(project().newResource(JavadocDirectoryType, destDir)); 210 return result; 211 } 212 213 private String tagletPath() { 214 return tagletpath.stream().<Path> mapMulti((e, consumer) -> { 215 if (e instanceof ClassTree classTree) { 216 consumer.accept(classTree.root()); 217 } else if (e instanceof JarFile jarFile) { 218 consumer.accept(jarFile.path()); 219 } 220 }).map(Path::toString).collect(Collectors.joining(File.pathSeparator)); 221 } 222}