View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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      * Would be record, but... Velocity have no idea what it is nor how to handle it.
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      * Builds "constant table" for one single class.
346      *
347      * Limitations:
348      * - works only for single class (no inherited constants)
349      * - does not work for fields that are Enum.name()
350      * - more to come
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 }