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