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 default -> throw new IllegalStateException();
156 };
157 discoveredKeys.put(
158 fieldValue.toString(),
159 new ConfigurationKey(
160 fieldValue.toString(),
161 values.get("defaultValue") != null
162 ? values.get("defaultValue")
163 .toString()
164 : null,
165 fqName,
166 desc,
167 since,
168 source,
169 type));
170 }
171 };
172 }
173 return null;
174 }
175 };
176 }
177 },
178 0);
179 } catch (IOException e) {
180 throw new RuntimeException(e);
181 }
182 }
183
184 static JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
185 Javadoc jd = (Javadoc) javaDoc.getInternal();
186 return new JavaDocImpl(javaDoc.getOrigin(), (Javadoc)
187 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel()), jd));
188 }
189
190 private static String unquote(String s) {
191 return (s.startsWith("\"") && s.endsWith("\"")) ? s.substring(1, s.length() - 1) : s;
192 }
193
194 private static JavaType<?> parse(Path path) {
195 try {
196 return Roaster.parse(path.toFile());
197 } catch (IOException e) {
198 throw new UncheckedIOException(e);
199 }
200 }
201
202 private static boolean toBoolean(String value) {
203 return ("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value));
204 }
205
206
207
208
209 public static class ConfigurationKey {
210 private final String key;
211 private final String defaultValue;
212 private final String fqName;
213 private final String description;
214 private final String since;
215 private final String configurationSource;
216 private final String configurationType;
217
218 @SuppressWarnings("checkstyle:parameternumber")
219 public ConfigurationKey(
220 String key,
221 String defaultValue,
222 String fqName,
223 String description,
224 String since,
225 String configurationSource,
226 String configurationType) {
227 this.key = key;
228 this.defaultValue = defaultValue;
229 this.fqName = fqName;
230 this.description = description;
231 this.since = since;
232 this.configurationSource = configurationSource;
233 this.configurationType = configurationType;
234 }
235
236 public String getKey() {
237 return key;
238 }
239
240 public String getDefaultValue() {
241 return defaultValue;
242 }
243
244 public String getFqName() {
245 return fqName;
246 }
247
248 public String getDescription() {
249 return description;
250 }
251
252 public String getSince() {
253 return since;
254 }
255
256 public String getConfigurationSource() {
257 return configurationSource;
258 }
259
260 public String getConfigurationType() {
261 return configurationType;
262 }
263 }
264
265 private static String nvl(String string, String def) {
266 return string == null ? def : string;
267 }
268
269 private static boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
270 return getTag(javaDocCapable, "@configurationSource") != null;
271 }
272
273 private static String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
274 String type = getTag(javaDocCapable, "@configurationType");
275 if (type != null) {
276 String linkPrefix = "{@link ";
277 String linkSuffix = "}";
278 if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
279 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
280 }
281 String javaLangPackage = "java.lang.";
282 if (type.startsWith(javaLangPackage)) {
283 type = type.substring(javaLangPackage.length());
284 }
285 }
286 return nvl(type, "n/a");
287 }
288
289 private static String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
290 String source = getTag(javaDocCapable, "@configurationSource");
291 if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
292 return "Session Configuration";
293 } else if ("{@link System#getProperty(String,String)}".equals(source)) {
294 return "Java System Properties";
295 } else if ("{@link org.apache.maven.api.model.Model#getProperties()}".equals(source)) {
296 return "Model Properties";
297 } else if ("{@link Session#getUserProperties()}".equals(source)) {
298 return "Session Properties";
299 } else {
300 return source;
301 }
302 }
303
304 private static String getSince(JavaDocCapable<?> javaDocCapable) {
305 List<JavaDocTag> tags;
306 if (javaDocCapable != null) {
307 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
308 tags = fieldSource.getJavaDoc().getTags("@since");
309 if (tags.isEmpty()) {
310 return getSince(fieldSource.getOrigin());
311 } else {
312 return tags.get(0).getValue();
313 }
314 } else if (javaDocCapable instanceof JavaClassSource classSource) {
315 tags = classSource.getJavaDoc().getTags("@since");
316 if (!tags.isEmpty()) {
317 return tags.get(0).getValue();
318 }
319 }
320 }
321 return "";
322 }
323
324 private static String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
325 List<JavaDocTag> tags;
326 if (javaDocCapable != null) {
327 if (javaDocCapable instanceof FieldSource<?> fieldSource) {
328 tags = fieldSource.getJavaDoc().getTags(tagName);
329 if (tags.isEmpty()) {
330 return getTag(fieldSource.getOrigin(), tagName);
331 } else {
332 return tags.get(0).getValue();
333 }
334 }
335 }
336 return null;
337 }
338
339 private static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
340
341 private static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
342
343
344
345
346
347
348
349
350
351 private static Map<String, String> extractConstants(Path file) {
352 StringWriter out = new StringWriter();
353 JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
354 Map<String, String> result = new HashMap<>();
355 out.getBuffer().toString().lines().forEach(l -> {
356 Matcher matcher = CONSTANT_PATTERN.matcher(l);
357 if (matcher.matches()) {
358 result.put(matcher.group(1), matcher.group(2));
359 }
360 });
361 return result;
362 }
363 }