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                                                             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      * Would be record, but... Velocity have no idea what it is nor how to handle it.
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      * Builds "constant table" for one single class.
345      *
346      * Limitations:
347      * - works only for single class (no inherited constants)
348      * - does not work for fields that are Enum.name()
349      * - more to come
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 }