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 Files.createDirectories(output.getParent());
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 = (values.get("source") != null
198 ? (String) values.get("source")
199 : "USER_PROPERTIES")
200 .toLowerCase();
201 source = switch (source) {
202 case "model" -> "Model properties";
203 case "user_properties" -> "User properties";
204 default -> source;
205 };
206 String type = (values.get("type") != null
207 ? (String) values.get("type")
208 : "java.lang.String");
209 if (type.startsWith("java.lang.")) {
210 type = type.substring("java.lang.".length());
211 } else if (type.startsWith("java.util.")) {
212 type = type.substring("java.util.".length());
213 }
214 discoveredKeys.add(Map.of(
215 KEY,
216 fieldValue.toString(),
217 "defaultValue",
218 values.get("defaultValue") != null
219 ? values.get("defaultValue")
220 .toString()
221 : "",
222 "fqName",
223 nvl(fqName, ""),
224 "description",
225 desc,
226 "since",
227 nvl(since, ""),
228 "configurationSource",
229 source,
230 "configurationType",
231 type));
232 }
233 };
234 }
235 return null;
236 }
237 };
238 }
239 },
240 0);
241 } catch (IOException e) {
242 throw new RuntimeException(e);
243 }
244 }
245
246 protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
247 JavaType<?> type = parse(path);
248 if (type instanceof JavaClassSource javaClassSource) {
249 javaClassSource.getFields().stream()
250 .filter(this::hasConfigurationSource)
251 .forEach(f -> {
252 Map<String, String> constants = extractConstants(Paths.get(path.toString()
253 .replace("/src/main/java/", "/target/classes/")
254 .replace(".java", ".class")));
255
256 String name = f.getName();
257 String key = constants.get(name);
258 String fqName = f.getOrigin().getCanonicalName() + "." + name;
259 String configurationType = getConfigurationType(f);
260 String defValue = getTag(f, "@configurationDefaultValue");
261 if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
262
263 String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
264 if (lookupValue == null) {
265
266
267
268 throw new IllegalArgumentException(
269 "Could not look up " + defValue + " for configuration " + fqName);
270 }
271 defValue = lookupValue;
272 if ("java.lang.Long".equals(configurationType)
273 && (defValue.endsWith("l") || defValue.endsWith("L"))) {
274 defValue = defValue.substring(0, defValue.length() - 1);
275 }
276 }
277 discoveredKeys.add(Map.of(
278 KEY,
279 key,
280 "defaultValue",
281 nvl(defValue, ""),
282 "fqName",
283 fqName,
284 "description",
285 cleanseJavadoc(f),
286 "since",
287 nvl(getSince(f), ""),
288 "configurationSource",
289 getConfigurationSource(f),
290 "configurationType",
291 configurationType,
292 "supportRepoIdSuffix",
293 toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
294 });
295 }
296 }
297
298 protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
299 Javadoc jd = (Javadoc) javaDoc.getInternal();
300 return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
301 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
302 }
303
304 protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
305 JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
306 String[] text = javaDoc.getFullText().split("\n");
307 StringBuilder result = new StringBuilder();
308 for (String line : text) {
309 if (!line.startsWith("@") && !line.trim().isEmpty()) {
310 result.append(line);
311 }
312 }
313 return cleanseTags(result.toString());
314 }
315
316 protected String cleanseTags(String text) {
317
318
319 Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
320 Matcher matcher = pattern.matcher(text);
321 if (!matcher.find()) {
322 return text;
323 }
324 int prevEnd = 0;
325 StringBuilder result = new StringBuilder();
326 do {
327 result.append(text, prevEnd, matcher.start(1));
328 result.append("<code>");
329 result.append(matcher.group(2));
330 result.append("</code>");
331 prevEnd = matcher.end(1);
332 } while (matcher.find());
333 result.append(text, prevEnd, text.length());
334 return result.toString();
335 }
336
337 protected JavaType<?> parse(Path path) {
338 try {
339 return Roaster.parse(path.toFile());
340 } catch (IOException e) {
341 throw new UncheckedIOException(e);
342 }
343 }
344
345 protected String toYesNo(String value) {
346 return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
347 }
348
349 protected String nvl(String string, String def) {
350 return string == null ? def : string;
351 }
352
353 protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
354 return getTag(javaDocCapable, "@configurationSource") != null;
355 }
356
357 protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
358 String type = getTag(javaDocCapable, "@configurationType");
359 if (type != null) {
360 String linkPrefix = "{@link ";
361 String linkSuffix = "}";
362 if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
363 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
364 }
365 String javaLangPackage = "java.lang.";
366 if (type.startsWith(javaLangPackage)) {
367 type = type.substring(javaLangPackage.length());
368 }
369 }
370 return nvl(type, "n/a");
371 }
372
373 protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
374 String source = getTag(javaDocCapable, "@configurationSource");
375 if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
376 return "Session Configuration";
377 } else if ("{@link System#getProperty(String,String)}".equals(source)) {
378 return "Java System Properties";
379 } else {
380 return source;
381 }
382 }
383
384 protected String getSince(JavaDocCapable<?> javaDocCapable) {
385 List<JavaDocTag> tags;
386 if (javaDocCapable != null) {
387 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
388 tags = fieldSource.getJavaDoc().getTags("@since");
389 if (tags.isEmpty()) {
390 return getSince(fieldSource.getOrigin());
391 } else {
392 return tags.get(0).getValue();
393 }
394 } else if (javaDocCapable instanceof JavaClassSource classSource) {
395 tags = classSource.getJavaDoc().getTags("@since");
396 if (!tags.isEmpty()) {
397 return tags.get(0).getValue();
398 }
399 }
400 }
401 return null;
402 }
403
404 protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
405 List<JavaDocTag> tags;
406 if (javaDocCapable != null) {
407 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
408 tags = fieldSource.getJavaDoc().getTags(tagName);
409 if (tags.isEmpty()) {
410 return getTag(fieldSource.getOrigin(), tagName);
411 } else {
412 return tags.get(0).getValue();
413 }
414 }
415 }
416 return null;
417 }
418
419 protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
420
421 protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
422
423
424
425
426
427
428
429
430
431 protected static Map<String, String> extractConstants(Path file) {
432 StringWriter out = new StringWriter();
433 JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
434 Map<String, String> result = new HashMap<>();
435 out.getBuffer().toString().lines().forEach(l -> {
436 Matcher matcher = CONSTANT_PATTERN.matcher(l);
437 if (matcher.matches()) {
438 result.put(matcher.group(1), matcher.group(2));
439 }
440 });
441 return result;
442 }
443 }