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 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 }