1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.tools;
20
21 import javax.tools.DiagnosticCollector;
22 import javax.tools.DocumentationTool;
23 import javax.tools.JavaFileObject;
24 import javax.tools.StandardJavaFileManager;
25 import javax.tools.StandardLocation;
26 import javax.tools.ToolProvider;
27
28 import java.io.File;
29 import java.io.IOException;
30 import java.io.PrintWriter;
31 import java.io.Reader;
32 import java.io.Writer;
33 import java.net.URI;
34 import java.net.URISyntaxException;
35 import java.net.URL;
36 import java.net.URLClassLoader;
37 import java.nio.charset.StandardCharsets;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.util.ArrayList;
41 import java.util.Arrays;
42 import java.util.Comparator;
43 import java.util.LinkedHashMap;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Objects;
47 import java.util.Properties;
48 import java.util.concurrent.Callable;
49 import java.util.stream.Collectors;
50 import java.util.stream.Stream;
51
52 import org.apache.velocity.VelocityContext;
53 import org.apache.velocity.app.VelocityEngine;
54 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
55 import org.codehaus.plexus.util.io.CachingWriter;
56 import picocli.CommandLine;
57
58
59
60
61
62
63
64
65 @CommandLine.Command(name = "docgen", description = "Configuration Documentation Generator")
66 public class CollectConfiguration implements Callable<Integer> {
67 public static void main(String[] args) {
68 new CommandLine(new CollectConfiguration()).execute(args);
69 }
70
71 protected static final String KEY = "key";
72
73
74
75
76 protected static final List<String> FIELDS = List.of(
77 KEY,
78 "defaultValue",
79 "fqName",
80 "description",
81 "since",
82 "configurationSource",
83 "configurationType",
84 "configurationTypeJavadocUrl",
85 "supportRepoIdSuffix",
86 "deprecated");
87
88
89
90
91 protected static final String CONFIGURATION_MARKER = "@configurationSource";
92
93
94
95
96
97 protected static final String MAVEN_CONFIGURATION_MARKER = "@Config";
98
99
100
101
102 public enum Mode {
103 maven,
104 resolver
105 }
106
107 @CommandLine.Option(
108 names = {"-m", "--mode"},
109 arity = "1",
110 paramLabel = "mode",
111 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'")
112 protected Mode mode = Mode.resolver;
113
114 @CommandLine.Option(
115 names = {"-t", "--templates"},
116 arity = "1",
117 split = ",",
118 paramLabel = "template",
119 description = "The template names to write content out without '.vm' extension")
120 protected List<String> templates;
121
122 @CommandLine.Option(
123 names = "--internal-javadoc-url",
124 paramLabel = "url",
125 description = "The base URL for Javadoc generated by this project")
126 protected URI internalJavadocUrl = URI.create("apidocs/");
127
128 @CommandLine.Option(
129 names = "--internal-javadoc-version",
130 paramLabel = "version",
131 description = "The Javadoc tool version used to generate the internal site")
132 protected String internalJavadocVersion = "21";
133
134 @CommandLine.Option(
135 names = "--external-javadoc-url",
136 split = ",",
137 paramLabel = "url",
138 description = "External Javadoc base URLs used for validated links")
139 protected List<URI> externalJavadocUrls = new ArrayList<>();
140
141 @CommandLine.Option(
142 names = "--internal-javadoc-source-tree",
143 paramLabel = "directory",
144 description =
145 "Recursively searches each directory tree for .java files and uses each distinct nearest ancestor "
146 + "ending in src/main/java as an aggregate Javadoc source root. May be repeated; defaults to "
147 + "the source processing root")
148 protected List<Path> internalJavadocSourceTrees = new ArrayList<>();
149
150 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from")
151 protected Path rootDirectory;
152
153 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to")
154 protected Path outputDirectory;
155
156 @Override
157 public Integer call() {
158 try {
159 rootDirectory = rootDirectory.toAbsolutePath().normalize();
160 outputDirectory = outputDirectory.toAbsolutePath().normalize();
161
162 System.out.println("Processing sources from " + rootDirectory);
163 Path intermediateFile = Files.createTempFile("configuration-keys", ".properties");
164 try {
165 runDoclet(intermediateFile);
166 List<Map<String, String>> discoveredKeys = readDiscoveredKeys(intermediateFile);
167 discoveredKeys.sort(Comparator.comparing(e -> e.get(KEY)));
168 render(discoveredKeys);
169 } finally {
170 Files.deleteIfExists(intermediateFile);
171 }
172 return 0;
173 } catch (Exception e) {
174 e.printStackTrace(System.err);
175 return 1;
176 }
177 }
178
179
180
181
182
183 protected void runDoclet(Path intermediateFile) throws Exception {
184
185
186
187 String marker = mode == Mode.maven ? MAVEN_CONFIGURATION_MARKER : CONFIGURATION_MARKER;
188 List<Path> javaSourceFiles = findMainJavaSourceFiles(rootDirectory);
189 List<File> sourceFiles = javaSourceFiles.stream()
190 .filter(p -> !p.getFileName().toString().equals("module-info.java"))
191 .filter(p -> !p.toString().replace('\\', '/').contains("/maven-resolver-tools/"))
192 .filter(p -> fileContains(p, marker))
193 .map(Path::toFile)
194 .collect(Collectors.toList());
195 if (sourceFiles.isEmpty()) {
196 throw new IllegalStateException(
197 "No Java sources declaring configuration keys found under " + rootDirectory);
198 }
199 List<Path> internalJavadocSourceFiles;
200 if (internalJavadocSourceTrees.isEmpty()) {
201 internalJavadocSourceFiles = javaSourceFiles;
202 } else {
203 internalJavadocSourceFiles = new ArrayList<>();
204 for (Path sourceTree : internalJavadocSourceTrees) {
205 internalJavadocSourceFiles.addAll(findMainJavaSourceFiles(sourceTree));
206 }
207 }
208
209 List<Path> internalJavadocSourceRoots = internalJavadocSourceFiles.stream()
210 .map(CollectConfiguration::findMainJavaSourceRoot)
211 .filter(Objects::nonNull)
212 .distinct()
213 .toList();
214
215 DocumentationTool documentationTool = ToolProvider.getSystemDocumentationTool();
216 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
217 try (StandardJavaFileManager fileManager =
218 documentationTool.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
219
220
221
222 fileManager.setLocation(StandardLocation.CLASS_PATH, resolveClasspath());
223
224 Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFiles);
225
226 List<String> options = new ArrayList<>(Arrays.asList(
227 "--output",
228 intermediateFile.toString(),
229 "--mode",
230 mode.name(),
231 "--internal-javadoc-url",
232 internalJavadocUrl.toString(),
233 "--internal-javadoc-version",
234 internalJavadocVersion,
235 "-encoding",
236 "UTF-8"));
237 for (Path internalJavadocSourceRoot : internalJavadocSourceRoots) {
238 options.add("--internal-javadoc-source-root");
239 options.add(internalJavadocSourceRoot.toString());
240 }
241 for (URI externalJavadocUrl : externalJavadocUrls) {
242 options.add("--external-javadoc-url");
243 options.add(externalJavadocUrl.toString());
244 }
245
246 Writer out = new PrintWriter(System.err);
247 DocumentationTool.DocumentationTask task = documentationTool.getTask(
248 out, fileManager, diagnostics, ConfigurationCollectorDoclet.class, options, compilationUnits);
249 boolean ok = task.call();
250 out.flush();
251 if (!ok) {
252 diagnostics.getDiagnostics().forEach(d -> System.err.println(d));
253 throw new IllegalStateException("Javadoc doclet execution failed");
254 }
255 }
256 }
257
258 private static boolean fileContains(Path path, String marker) {
259 try {
260 return Files.readString(path, StandardCharsets.UTF_8).contains(marker);
261 } catch (IOException e) {
262 return false;
263 }
264 }
265
266 private static List<Path> findMainJavaSourceFiles(Path sourceTree) throws IOException {
267 try (Stream<Path> stream = Files.walk(sourceTree)) {
268 return stream.map(Path::toAbsolutePath)
269 .filter(p -> p.getFileName().toString().endsWith(".java"))
270 .filter(p -> findMainJavaSourceRoot(p) != null)
271 .toList();
272 }
273 }
274
275 private static Path findMainJavaSourceRoot(Path sourceFile) {
276 Path suffix = Path.of("src", "main", "java");
277 for (Path current = sourceFile.getParent(); current != null; current = current.getParent()) {
278 if (current.endsWith(suffix)) {
279 return current;
280 }
281 }
282 return null;
283 }
284
285
286
287
288
289
290 private static List<File> resolveClasspath() {
291 List<File> classpath = new ArrayList<>();
292 for (ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl != null; cl = cl.getParent()) {
293 if (cl instanceof URLClassLoader) {
294 for (URL url : ((URLClassLoader) cl).getURLs()) {
295 if ("file".equals(url.getProtocol())) {
296 try {
297 classpath.add(new File(url.toURI()));
298 } catch (URISyntaxException e) {
299 classpath.add(new File(url.getPath()));
300 }
301 }
302 }
303 }
304 }
305 for (String element : System.getProperty("java.class.path").split(File.pathSeparator)) {
306 classpath.add(new File(element));
307 }
308 return classpath;
309 }
310
311
312
313
314
315 static List<Map<String, String>> readDiscoveredKeys(Path intermediateFile) throws Exception {
316 Properties properties = new Properties();
317 try (Reader reader = Files.newBufferedReader(intermediateFile, StandardCharsets.UTF_8)) {
318 properties.load(reader);
319 }
320 int count = Integer.parseInt(properties.getProperty("keys.count", "0"));
321 List<Map<String, String>> discoveredKeys = new ArrayList<>(count);
322 for (int i = 0; i < count; i++) {
323 Map<String, String> entry = new LinkedHashMap<>();
324 for (String field : FIELDS) {
325 entry.put(field, properties.getProperty("keys." + i + "." + field, ""));
326 }
327 discoveredKeys.add(entry);
328 }
329 return discoveredKeys;
330 }
331
332 protected void render(List<Map<String, String>> discoveredKeys) throws Exception {
333 Properties properties = new Properties();
334 properties.setProperty("resource.loaders", "classpath");
335 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
336 VelocityEngine velocityEngine = new VelocityEngine();
337 velocityEngine.init(properties);
338
339 VelocityContext context = new VelocityContext();
340 context.put("keys", discoveredKeys);
341
342 for (String template : templates) {
343 Path output = outputDirectory.resolve(template);
344 Files.createDirectories(output.getParent());
345 System.out.println("Writing out to " + output);
346 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
347 velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
348 }
349 }
350 }
351 }