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 java.io.IOException;
22 import java.io.PrintWriter;
23 import java.io.StringWriter;
24 import java.io.UncheckedIOException;
25 import java.io.Writer;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.nio.file.Paths;
30 import java.util.ArrayList;
31 import java.util.Collections;
32 import java.util.Comparator;
33 import java.util.HashMap;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Properties;
37 import java.util.concurrent.Callable;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 import java.util.spi.ToolProvider;
41 import java.util.stream.Stream;
42
43 import org.apache.velocity.VelocityContext;
44 import org.apache.velocity.app.VelocityEngine;
45 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
46 import org.codehaus.plexus.util.io.CachingWriter;
47 import org.jboss.forge.roaster.Roaster;
48 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
49 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
50 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
51 import org.jboss.forge.roaster.model.JavaDoc;
52 import org.jboss.forge.roaster.model.JavaDocCapable;
53 import org.jboss.forge.roaster.model.JavaDocTag;
54 import org.jboss.forge.roaster.model.JavaType;
55 import org.jboss.forge.roaster.model.impl.JavaDocImpl;
56 import org.jboss.forge.roaster.model.source.FieldSource;
57 import org.jboss.forge.roaster.model.source.JavaClassSource;
58 import org.jboss.forge.roaster.model.source.JavaDocSource;
59 import org.objectweb.asm.AnnotationVisitor;
60 import org.objectweb.asm.ClassReader;
61 import org.objectweb.asm.ClassVisitor;
62 import org.objectweb.asm.FieldVisitor;
63 import org.objectweb.asm.Opcodes;
64 import picocli.CommandLine;
65
66 @CommandLine.Command(name = "docgen", description = "Maven Documentation Generator")
67 public class CollectConfiguration implements Callable<Integer> {
68 public static void main(String[] args) {
69 new CommandLine(new CollectConfiguration()).execute(args);
70 }
71
72 protected static final String KEY = "key";
73
74 enum Mode {
75 maven,
76 resolver
77 }
78
79 @CommandLine.Option(
80 names = {"-m", "--mode"},
81 arity = "1",
82 paramLabel = "mode",
83 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'")
84 protected Mode mode;
85
86 @CommandLine.Option(
87 names = {"-t", "--templates"},
88 arity = "1",
89 split = ",",
90 paramLabel = "template",
91 description = "The template names to write content out without '.vm' extension")
92 protected List<String> templates;
93
94 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from")
95 protected Path rootDirectory;
96
97 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to")
98 protected Path outputDirectory;
99
100 @Override
101 public Integer call() {
102 try {
103 rootDirectory = rootDirectory.toAbsolutePath().normalize();
104 outputDirectory = outputDirectory.toAbsolutePath().normalize();
105
106 ArrayList<Map<String, String>> discoveredKeys = new ArrayList<>();
107 try (Stream<Path> stream = Files.walk(rootDirectory)) {
108 if (mode == Mode.maven) {
109 System.out.println("Processing Maven sources from " + rootDirectory);
110 stream.map(Path::toAbsolutePath)
111 .filter(p -> p.getFileName().toString().endsWith(".class"))
112 .filter(p -> p.toString().contains("/target/classes/"))
113 .forEach(p -> {
114 processMavenClass(p, discoveredKeys);
115 });
116 } else if (mode == Mode.resolver) {
117 System.out.println("Processing Resolver sources from " + rootDirectory);
118 stream.map(Path::toAbsolutePath)
119 .filter(p -> p.getFileName().toString().endsWith(".java"))
120 .filter(p -> p.toString().contains("/src/main/java/"))
121 .filter(p -> !p.toString().endsWith("/module-info.java"))
122 .forEach(p -> processResolverClass(p, discoveredKeys));
123 } else {
124 throw new IllegalStateException("Unsupported mode " + mode);
125 }
126 }
127
128 Collections.sort(discoveredKeys, Comparator.comparing(e -> e.get(KEY)));
129
130 Properties properties = new Properties();
131 properties.setProperty("resource.loaders", "classpath");
132 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
133 VelocityEngine velocityEngine = new VelocityEngine();
134 velocityEngine.init(properties);
135
136 VelocityContext context = new VelocityContext();
137 context.put("keys", discoveredKeys);
138
139 for (String template : templates) {
140 Path output = outputDirectory.resolve(template);
141 System.out.println("Writing out to " + output);
142 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
143 velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
144 }
145 }
146 return 0;
147 } catch (Exception e) {
148 e.printStackTrace(System.err);
149 return 1;
150 }
151 }
152
153 protected void processMavenClass(Path path, List<Map<String, String>> discoveredKeys) {
154 try {
155 ClassReader classReader = new ClassReader(Files.newInputStream(path));
156 classReader.accept(
157 new ClassVisitor(Opcodes.ASM9) {
158 @Override
159 public FieldVisitor visitField(
160 int fieldAccess,
161 String fieldName,
162 String fieldDescriptor,
163 String fieldSignature,
164 Object fieldValue) {
165 return new FieldVisitor(Opcodes.ASM9) {
166 @Override
167 public AnnotationVisitor visitAnnotation(
168 String annotationDescriptor, boolean annotationVisible) {
169 if (annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
170 return new AnnotationVisitor(Opcodes.ASM9) {
171 final Map<String, Object> values = new HashMap<>();
172
173 @Override
174 public void visit(String name, Object value) {
175 values.put(name, value);
176 }
177
178 @Override
179 public void visitEnum(String name, String descriptor, String value) {
180 values.put(name, value);
181 }
182
183 @Override
184 public void visitEnd() {
185 JavaType<?> jtype = parse(Paths.get(path.toString()
186 .replace("/target/classes/", "/src/main/java/")
187 .replace(".class", ".java")));
188 FieldSource<JavaClassSource> f =
189 ((JavaClassSource) jtype).getField(fieldName);
190
191 String fqName = null;
192 String desc = cloneJavadoc(f.getJavaDoc())
193 .removeAllTags()
194 .getFullText()
195 .replace("*", "\\*");
196 String since = getSince(f);
197 String source =
198 switch ((values.get("source") != null
199 ? (String) values.get("source")
200 : "USER_PROPERTIES")
201 .toLowerCase()) {
202 case "model" -> "Model properties";
203 case "user_properties" -> "User properties";
204 default -> throw new IllegalStateException();
205 };
206 String type =
207 switch ((values.get("type") != null
208 ? (String) values.get("type")
209 : "java.lang.String")) {
210 case "java.lang.String" -> "String";
211 case "java.lang.Integer" -> "Integer";
212 case "java.lang.Boolean" -> "Boolean";
213 default -> throw new IllegalStateException();
214 };
215 discoveredKeys.add(Map.of(
216 KEY,
217 fieldValue.toString(),
218 "defaultValue",
219 values.get("defaultValue") != null
220 ? values.get("defaultValue")
221 .toString()
222 : "",
223 "fqName",
224 nvl(fqName, ""),
225 "description",
226 desc,
227 "since",
228 nvl(since, ""),
229 "configurationSource",
230 source,
231 "configurationType",
232 type));
233 }
234 };
235 }
236 return null;
237 }
238 };
239 }
240 },
241 0);
242 } catch (IOException e) {
243 throw new RuntimeException(e);
244 }
245 }
246
247 protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
248 JavaType<?> type = parse(path);
249 if (type instanceof JavaClassSource javaClassSource) {
250 javaClassSource.getFields().stream()
251 .filter(this::hasConfigurationSource)
252 .forEach(f -> {
253 Map<String, String> constants = extractConstants(Paths.get(path.toString()
254 .replace("/src/main/java/", "/target/classes/")
255 .replace(".java", ".class")));
256
257 String name = f.getName();
258 String key = constants.get(name);
259 String fqName = f.getOrigin().getCanonicalName() + "." + name;
260 String configurationType = getConfigurationType(f);
261 String defValue = getTag(f, "@configurationDefaultValue");
262 if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
263
264 String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
265 if (lookupValue == null) {
266
267
268
269 throw new IllegalArgumentException(
270 "Could not look up " + defValue + " for configuration " + fqName);
271 }
272 defValue = lookupValue;
273 if ("java.lang.Long".equals(configurationType)
274 && (defValue.endsWith("l") || defValue.endsWith("L"))) {
275 defValue = defValue.substring(0, defValue.length() - 1);
276 }
277 }
278 discoveredKeys.add(Map.of(
279 KEY,
280 key,
281 "defaultValue",
282 nvl(defValue, ""),
283 "fqName",
284 fqName,
285 "description",
286 cleanseJavadoc(f),
287 "since",
288 nvl(getSince(f), ""),
289 "configurationSource",
290 getConfigurationSource(f),
291 "configurationType",
292 configurationType,
293 "supportRepoIdSuffix",
294 toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
295 });
296 }
297 }
298
299 protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
300 Javadoc jd = (Javadoc) javaDoc.getInternal();
301 return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
302 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
303 }
304
305 protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
306 JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
307 String[] text = javaDoc.getFullText().split("\n");
308 StringBuilder result = new StringBuilder();
309 for (String line : text) {
310 if (!line.startsWith("@") && !line.trim().isEmpty()) {
311 result.append(line);
312 }
313 }
314 return cleanseTags(result.toString());
315 }
316
317 protected String cleanseTags(String text) {
318
319
320 Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
321 Matcher matcher = pattern.matcher(text);
322 if (!matcher.find()) {
323 return text;
324 }
325 int prevEnd = 0;
326 StringBuilder result = new StringBuilder();
327 do {
328 result.append(text, prevEnd, matcher.start(1));
329 result.append("<code>");
330 result.append(matcher.group(2));
331 result.append("</code>");
332 prevEnd = matcher.end(1);
333 } while (matcher.find());
334 result.append(text, prevEnd, text.length());
335 return result.toString();
336 }
337
338 protected JavaType<?> parse(Path path) {
339 try {
340 return Roaster.parse(path.toFile());
341 } catch (IOException e) {
342 throw new UncheckedIOException(e);
343 }
344 }
345
346 protected String toYesNo(String value) {
347 return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
348 }
349
350 protected String nvl(String string, String def) {
351 return string == null ? def : string;
352 }
353
354 protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
355 return getTag(javaDocCapable, "@configurationSource") != null;
356 }
357
358 protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
359 String type = getTag(javaDocCapable, "@configurationType");
360 if (type != null) {
361 String linkPrefix = "{@link ";
362 String linkSuffix = "}";
363 if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
364 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
365 }
366 String javaLangPackage = "java.lang.";
367 if (type.startsWith(javaLangPackage)) {
368 type = type.substring(javaLangPackage.length());
369 }
370 }
371 return nvl(type, "n/a");
372 }
373
374 protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
375 String source = getTag(javaDocCapable, "@configurationSource");
376 if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
377 return "Session Configuration";
378 } else if ("{@link System#getProperty(String,String)}".equals(source)) {
379 return "Java System Properties";
380 } else {
381 return source;
382 }
383 }
384
385 protected String getSince(JavaDocCapable<?> javaDocCapable) {
386 List<JavaDocTag> tags;
387 if (javaDocCapable != null) {
388 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
389 tags = fieldSource.getJavaDoc().getTags("@since");
390 if (tags.isEmpty()) {
391 return getSince(fieldSource.getOrigin());
392 } else {
393 return tags.get(0).getValue();
394 }
395 } else if (javaDocCapable instanceof JavaClassSource classSource) {
396 tags = classSource.getJavaDoc().getTags("@since");
397 if (!tags.isEmpty()) {
398 return tags.get(0).getValue();
399 }
400 }
401 }
402 return null;
403 }
404
405 protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
406 List<JavaDocTag> tags;
407 if (javaDocCapable != null) {
408 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
409 tags = fieldSource.getJavaDoc().getTags(tagName);
410 if (tags.isEmpty()) {
411 return getTag(fieldSource.getOrigin(), tagName);
412 } else {
413 return tags.get(0).getValue();
414 }
415 }
416 }
417 return null;
418 }
419
420 protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
421
422 protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
423
424
425
426
427
428
429
430
431
432 protected static Map<String, String> extractConstants(Path file) {
433 StringWriter out = new StringWriter();
434 JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
435 Map<String, String> result = new HashMap<>();
436 out.getBuffer().toString().lines().forEach(l -> {
437 Matcher matcher = CONSTANT_PATTERN.matcher(l);
438 if (matcher.matches()) {
439 result.put(matcher.group(1), matcher.group(2));
440 }
441 });
442 return result;
443 }
444 }