1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.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.HashMap;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Properties;
34 import java.util.TreeMap;
35 import java.util.regex.Matcher;
36 import java.util.regex.Pattern;
37 import java.util.spi.ToolProvider;
38
39 import org.apache.maven.api.annotations.Config;
40 import org.apache.velocity.VelocityContext;
41 import org.apache.velocity.app.VelocityEngine;
42 import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
43 import org.codehaus.plexus.util.io.CachingWriter;
44 import org.jboss.forge.roaster.Roaster;
45 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
46 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
47 import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
48 import org.jboss.forge.roaster.model.JavaDocCapable;
49 import org.jboss.forge.roaster.model.JavaDocTag;
50 import org.jboss.forge.roaster.model.JavaType;
51 import org.jboss.forge.roaster.model.impl.JavaDocImpl;
52 import org.jboss.forge.roaster.model.source.FieldSource;
53 import org.jboss.forge.roaster.model.source.JavaClassSource;
54 import org.jboss.forge.roaster.model.source.JavaDocSource;
55 import org.objectweb.asm.AnnotationVisitor;
56 import org.objectweb.asm.ClassReader;
57 import org.objectweb.asm.ClassVisitor;
58 import org.objectweb.asm.FieldVisitor;
59 import org.objectweb.asm.Opcodes;
60
61 public class CollectConfiguration {
62
63 public static void main(String[] args) throws Exception {
64 try {
65 Path start = Paths.get(args.length > 0 ? args[0] : ".");
66 Path output = Paths.get(args.length > 1 ? args[1] : "output");
67
68 TreeMap<String, ConfigurationKey> discoveredKeys = new TreeMap<>();
69
70 Files.walk(start)
71 .map(Path::toAbsolutePath)
72 .filter(p -> p.getFileName().toString().endsWith(".class"))
73 .filter(p -> p.toString().contains("/target/classes/"))
74 .forEach(p -> {
75 processClass(p, discoveredKeys);
76 });
77
78 VelocityEngine velocityEngine = new VelocityEngine();
79 Properties properties = new Properties();
80 properties.setProperty("resource.loaders", "classpath");
81 properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
82 velocityEngine.init(properties);
83
84 VelocityContext context = new VelocityContext();
85 context.put("keys", discoveredKeys.values());
86
87 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
88 velocityEngine.getTemplate("page.vm").merge(context, fileWriter);
89 }
90 } catch (Throwable t) {
91 t.printStackTrace();
92 throw t;
93 }
94 }
95
96 private static void processClass(Path path, Map<String, ConfigurationKey> discoveredKeys) {
97 try {
98 ClassReader classReader = new ClassReader(Files.newInputStream(path));
99 classReader.accept(
100 new ClassVisitor(Opcodes.ASM9) {
101 @Override
102 public FieldVisitor visitField(
103 int fieldAccess,
104 String fieldName,
105 String fieldDescriptor,
106 String fieldSignature,
107 Object fieldValue) {
108 return new FieldVisitor(Opcodes.ASM9) {
109 @Override
110 public AnnotationVisitor visitAnnotation(
111 String annotationDescriptor, boolean annotationVisible) {
112 if (annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
113 return new AnnotationVisitor(Opcodes.ASM9) {
114 Map<String, Object> values = new HashMap<>();
115
116 @Override
117 public void visit(String name, Object value) {
118 values.put(name, value);
119 }
120
121 @Override
122 public void visitEnum(String name, String descriptor, String value) {
123 values.put(name, value);
124 }
125
126 @Override
127 public void visitEnd() {
128 JavaType<?> jtype = parse(Paths.get(path.toString()
129 .replace("/target/classes/", "/src/main/java/")
130 .replace(".class", ".java")));
131 FieldSource<JavaClassSource> f =
132 ((JavaClassSource) jtype).getField(fieldName);
133
134 String fqName = null;
135 String desc = cloneJavadoc(f.getJavaDoc())
136 .removeAllTags()
137 .getFullText()
138 .replace("*", "\\*");
139 String since = getSince(f);
140 String source =
141 switch ((values.get("source") != null
142 ? (String) values.get("source")
143 : Config.Source.USER_PROPERTIES.toString())
144 .toLowerCase()) {
145 case "model" -> "Model properties";
146 case "user_properties" -> "User properties";
147 default -> throw new IllegalStateException();
148 };
149 String type =
150 switch ((values.get("type") != null
151 ? (String) values.get("type")
152 : "java.lang.String")) {
153 case "java.lang.String" -> "String";
154 case "java.lang.Integer" -> "Integer";
155 case "java.lang.Boolean" -> "Boolean";
156 default -> throw new IllegalStateException();
157 };
158 discoveredKeys.put(
159 fieldValue.toString(),
160 new ConfigurationKey(
161 fieldValue.toString(),
162 values.get("defaultValue") != null
163 ? values.get("defaultValue")
164 .toString()
165 : null,
166 fqName,
167 desc,
168 since,
169 source,
170 type));
171 }
172 };
173 }
174 return null;
175 }
176 };
177 }
178 },
179 0);
180 } catch (IOException e) {
181 throw new RuntimeException(e);
182 }
183 }
184
185 static JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
186 Javadoc jd = (Javadoc) javaDoc.getInternal();
187 return new JavaDocImpl(javaDoc.getOrigin(), (Javadoc)
188 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel()), jd));
189 }
190
191 private static String unquote(String s) {
192 return (s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, s.length() - 1) : s;
193 }
194
195 private static JavaType<?> parse(Path path) {
196 try {
197 return Roaster.parse(path.toFile());
198 } catch (IOException e) {
199 throw new UncheckedIOException(e);
200 }
201 }
202
203 private static boolean toBoolean(String value) {
204 return ("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value));
205 }
206
207
208
209
210 public static class ConfigurationKey {
211 private final String key;
212 private final String defaultValue;
213 private final String fqName;
214 private final String description;
215 private final String since;
216 private final String configurationSource;
217 private final String configurationType;
218
219 @SuppressWarnings("checkstyle:parameternumber")
220 public ConfigurationKey(
221 String key,
222 String defaultValue,
223 String fqName,
224 String description,
225 String since,
226 String configurationSource,
227 String configurationType) {
228 this.key = key;
229 this.defaultValue = defaultValue;
230 this.fqName = fqName;
231 this.description = description;
232 this.since = since;
233 this.configurationSource = configurationSource;
234 this.configurationType = configurationType;
235 }
236
237 public String getKey() {
238 return key;
239 }
240
241 public String getDefaultValue() {
242 return defaultValue;
243 }
244
245 public String getFqName() {
246 return fqName;
247 }
248
249 public String getDescription() {
250 return description;
251 }
252
253 public String getSince() {
254 return since;
255 }
256
257 public String getConfigurationSource() {
258 return configurationSource;
259 }
260
261 public String getConfigurationType() {
262 return configurationType;
263 }
264 }
265
266 private static String nvl(String string, String def) {
267 return string == null ? def : string;
268 }
269
270 private static boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
271 return getTag(javaDocCapable, "@configurationSource") != null;
272 }
273
274 private static String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
275 String type = getTag(javaDocCapable, "@configurationType");
276 if (type != null) {
277 String linkPrefix = "{@link ";
278 String linkSuffix = "}";
279 if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
280 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
281 }
282 String javaLangPackage = "java.lang.";
283 if (type.startsWith(javaLangPackage)) {
284 type = type.substring(javaLangPackage.length());
285 }
286 }
287 return nvl(type, "n/a");
288 }
289
290 private static String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
291 String source = getTag(javaDocCapable, "@configurationSource");
292 if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
293 return "Session Configuration";
294 } else if ("{@link System#getProperty(String,String)}".equals(source)) {
295 return "Java System Properties";
296 } else if ("{@link org.apache.maven.api.model.Model#getProperties()}".equals(source)) {
297 return "Model Properties";
298 } else if ("{@link Session#getUserProperties()}".equals(source)) {
299 return "Session Properties";
300 } else {
301 return source;
302 }
303 }
304
305 private static String getSince(JavaDocCapable<?> javaDocCapable) {
306 List<JavaDocTag> tags;
307 if (javaDocCapable != null) {
308 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
309 tags = fieldSource.getJavaDoc().getTags("@since");
310 if (tags.isEmpty()) {
311 return getSince(fieldSource.getOrigin());
312 } else {
313 return tags.get(0).getValue();
314 }
315 } else if (javaDocCapable instanceof JavaClassSource classSource) {
316 tags = classSource.getJavaDoc().getTags("@since");
317 if (!tags.isEmpty()) {
318 return tags.get(0).getValue();
319 }
320 }
321 }
322 return "";
323 }
324
325 private static String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
326 List<JavaDocTag> tags;
327 if (javaDocCapable != null) {
328 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
329 tags = fieldSource.getJavaDoc().getTags(tagName);
330 if (tags.isEmpty()) {
331 return getTag(fieldSource.getOrigin(), tagName);
332 } else {
333 return tags.get(0).getValue();
334 }
335 }
336 }
337 return null;
338 }
339
340 private static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
341
342 private static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
343
344
345
346
347
348
349
350
351
352 private static Map<String, String> extractConstants(Path file) {
353 StringWriter out = new StringWriter();
354 JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
355 Map<String, String> result = new HashMap<>();
356 out.getBuffer().toString().lines().forEach(l -> {
357 Matcher matcher = CONSTANT_PATTERN.matcher(l);
358 if (matcher.matches()) {
359 result.put(matcher.group(1), matcher.group(2));
360 }
361 });
362 return result;
363 }
364 }