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