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.URISyntaxException;
34 import java.net.URL;
35 import java.net.URLClassLoader;
36 import java.nio.charset.StandardCharsets;
37 import java.nio.file.Files;
38 import java.nio.file.Path;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Comparator;
42 import java.util.LinkedHashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Properties;
46 import java.util.concurrent.Callable;
47 import java.util.stream.Collectors;
48 import java.util.stream.Stream;
49
50 import org.apache.velocity.VelocityContext;
51 import org.apache.velocity.app.VelocityEngine;
52 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
53 import org.codehaus.plexus.util.io.CachingWriter;
54 import picocli.CommandLine;
55
56
57
58
59
60
61
62
63 @CommandLine.Command(name = "docgen", description = "Configuration Documentation Generator")
64 public class CollectConfiguration implements Callable<Integer> {
65 public static void main(String[] args) {
66 new CommandLine(new CollectConfiguration()).execute(args);
67 }
68
69 protected static final String KEY = "key";
70
71
72
73
74 protected static final List<String> FIELDS = List.of(
75 KEY,
76 "defaultValue",
77 "fqName",
78 "description",
79 "since",
80 "configurationSource",
81 "configurationType",
82 "supportRepoIdSuffix",
83 "deprecated");
84
85
86
87
88 protected static final String CONFIGURATION_MARKER = "@configurationSource";
89
90
91
92
93
94 protected static final String MAVEN_CONFIGURATION_MARKER = "@Config";
95
96
97
98
99 public enum Mode {
100 maven,
101 resolver
102 }
103
104 @CommandLine.Option(
105 names = {"-m", "--mode"},
106 arity = "1",
107 paramLabel = "mode",
108 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'")
109 protected Mode mode = Mode.resolver;
110
111 @CommandLine.Option(
112 names = {"-t", "--templates"},
113 arity = "1",
114 split = ",",
115 paramLabel = "template",
116 description = "The template names to write content out without '.vm' extension")
117 protected List<String> templates;
118
119 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from")
120 protected Path rootDirectory;
121
122 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to")
123 protected Path outputDirectory;
124
125 @Override
126 public Integer call() {
127 try {
128 rootDirectory = rootDirectory.toAbsolutePath().normalize();
129 outputDirectory = outputDirectory.toAbsolutePath().normalize();
130
131 System.out.println("Processing sources from " + rootDirectory);
132 Path intermediateFile = Files.createTempFile("configuration-keys", ".properties");
133 try {
134 runDoclet(intermediateFile);
135 List<Map<String, String>> discoveredKeys = readDiscoveredKeys(intermediateFile);
136 discoveredKeys.sort(Comparator.comparing(e -> e.get(KEY)));
137 render(discoveredKeys);
138 } finally {
139 Files.deleteIfExists(intermediateFile);
140 }
141 return 0;
142 } catch (Exception e) {
143 e.printStackTrace(System.err);
144 return 1;
145 }
146 }
147
148
149
150
151
152 protected void runDoclet(Path intermediateFile) throws Exception {
153
154
155
156 String marker = mode == Mode.maven ? MAVEN_CONFIGURATION_MARKER : CONFIGURATION_MARKER;
157 List<File> sourceFiles;
158 try (Stream<Path> stream = Files.walk(rootDirectory)) {
159 sourceFiles = stream.map(Path::toAbsolutePath)
160 .filter(p -> p.getFileName().toString().endsWith(".java"))
161 .filter(p -> p.toString().contains("/src/main/java/"))
162 .filter(p -> !p.toString().endsWith("/module-info.java"))
163 .filter(p -> !p.toString().contains("/maven-resolver-tools/"))
164 .filter(p -> fileContains(p, marker))
165 .map(Path::toFile)
166 .collect(Collectors.toList());
167 }
168 if (sourceFiles.isEmpty()) {
169 throw new IllegalStateException(
170 "No Java sources declaring configuration keys found under " + rootDirectory);
171 }
172
173 DocumentationTool documentationTool = ToolProvider.getSystemDocumentationTool();
174 DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
175 try (StandardJavaFileManager fileManager =
176 documentationTool.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
177
178
179
180 fileManager.setLocation(StandardLocation.CLASS_PATH, resolveClasspath());
181
182 Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(sourceFiles);
183
184 List<String> options = new ArrayList<>(Arrays.asList(
185 "--output", intermediateFile.toString(), "--mode", mode.name(), "-encoding", "UTF-8"));
186
187 Writer out = new PrintWriter(System.err);
188 DocumentationTool.DocumentationTask task = documentationTool.getTask(
189 out, fileManager, diagnostics, ConfigurationCollectorDoclet.class, options, compilationUnits);
190 boolean ok = task.call();
191 out.flush();
192 if (!ok) {
193 diagnostics.getDiagnostics().forEach(d -> System.err.println(d));
194 throw new IllegalStateException("Javadoc doclet execution failed");
195 }
196 }
197 }
198
199 private static boolean fileContains(Path path, String marker) {
200 try {
201 return Files.readString(path, StandardCharsets.UTF_8).contains(marker);
202 } catch (IOException e) {
203 return false;
204 }
205 }
206
207
208
209
210
211
212 private static List<File> resolveClasspath() {
213 List<File> classpath = new ArrayList<>();
214 for (ClassLoader cl = Thread.currentThread().getContextClassLoader(); cl != null; cl = cl.getParent()) {
215 if (cl instanceof URLClassLoader) {
216 for (URL url : ((URLClassLoader) cl).getURLs()) {
217 if ("file".equals(url.getProtocol())) {
218 try {
219 classpath.add(new File(url.toURI()));
220 } catch (URISyntaxException e) {
221 classpath.add(new File(url.getPath()));
222 }
223 }
224 }
225 }
226 }
227 for (String element : System.getProperty("java.class.path").split(File.pathSeparator)) {
228 classpath.add(new File(element));
229 }
230 return classpath;
231 }
232
233
234
235
236
237 static List<Map<String, String>> readDiscoveredKeys(Path intermediateFile) throws Exception {
238 Properties properties = new Properties();
239 try (Reader reader = Files.newBufferedReader(intermediateFile, StandardCharsets.UTF_8)) {
240 properties.load(reader);
241 }
242 int count = Integer.parseInt(properties.getProperty("keys.count", "0"));
243 List<Map<String, String>> discoveredKeys = new ArrayList<>(count);
244 for (int i = 0; i < count; i++) {
245 Map<String, String> entry = new LinkedHashMap<>();
246 for (String field : FIELDS) {
247 entry.put(field, properties.getProperty("keys." + i + "." + field, ""));
248 }
249 discoveredKeys.add(entry);
250 }
251 return discoveredKeys;
252 }
253
254 protected void render(List<Map<String, String>> discoveredKeys) throws Exception {
255 Properties properties = new Properties();
256 properties.setProperty("resource.loaders", "classpath");
257 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
258 VelocityEngine velocityEngine = new VelocityEngine();
259 velocityEngine.init(properties);
260
261 VelocityContext context = new VelocityContext();
262 context.put("keys", discoveredKeys);
263
264 for (String template : templates) {
265 Path output = outputDirectory.resolve(template);
266 Files.createDirectories(output.getParent());
267 System.out.println("Writing out to " + output);
268 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
269 velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
270 }
271 }
272 }
273 }