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