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.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                 Files.createDirectories(output.getParent());
141                 System.out.println("Writing out to " + output);
142                 try (Writer fileWriter = new CachingWriter(output, StandardCharsets.UTF_8)) {
143                     velocityEngine.getTemplate(template + ".vm").merge(context, fileWriter);
144                 }
145             }
146             return 0;
147         } catch (Exception e) {
148             e.printStackTrace(System.err);
149             return 1;
150         }
151     }
152 
153     protected void processMavenClass(Path path, List<Map<String, String>> discoveredKeys) {
154         try {
155             ClassReader classReader = new ClassReader(Files.newInputStream(path));
156             classReader.accept(
157                     new ClassVisitor(Opcodes.ASM9) {
158                         @Override
159                         public FieldVisitor visitField(
160                                 int fieldAccess,
161                                 String fieldName,
162                                 String fieldDescriptor,
163                                 String fieldSignature,
164                                 Object fieldValue) {
165                             return new FieldVisitor(Opcodes.ASM9) {
166                                 @Override
167                                 public AnnotationVisitor visitAnnotation(
168                                         String annotationDescriptor, boolean annotationVisible) {
169                                     if (annotationDescriptor.equals("Lorg/apache/maven/api/annotations/Config;")) {
170                                         return new AnnotationVisitor(Opcodes.ASM9) {
171                                             final Map<String, Object> values = new HashMap<>();
172 
173                                             @Override
174                                             public void visit(String name, Object value) {
175                                                 values.put(name, value);
176                                             }
177 
178                                             @Override
179                                             public void visitEnum(String name, String descriptor, String value) {
180                                                 values.put(name, value);
181                                             }
182 
183                                             @Override
184                                             public void visitEnd() {
185                                                 JavaType<?> jtype = parse(Paths.get(path.toString()
186                                                         .replace("/target/classes/", "/src/main/java/")
187                                                         .replace(".class", ".java")));
188                                                 FieldSource<JavaClassSource> f =
189                                                         ((JavaClassSource) jtype).getField(fieldName);
190 
191                                                 String fqName = null;
192                                                 String desc = cloneJavadoc(f.getJavaDoc())
193                                                         .removeAllTags()
194                                                         .getFullText()
195                                                         .replace("*", "\\*");
196                                                 String since = getSince(f);
197                                                 String source = (values.get("source") != null
198                                                                 ? (String) values.get("source")
199                                                                 : "USER_PROPERTIES") // TODO: enum
200                                                         .toLowerCase();
201                                                 source = switch (source) {
202                                                     case "model" -> "Model properties";
203                                                     case "user_properties" -> "User properties";
204                                                     default -> source;
205                                                 };
206                                                 String type = (values.get("type") != null
207                                                         ? (String) values.get("type")
208                                                         : "java.lang.String");
209                                                 if (type.startsWith("java.lang.")) {
210                                                     type = type.substring("java.lang.".length());
211                                                 } else if (type.startsWith("java.util.")) {
212                                                     type = type.substring("java.util.".length());
213                                                 }
214                                                 discoveredKeys.add(Map.of(
215                                                         KEY,
216                                                         fieldValue.toString(),
217                                                         "defaultValue",
218                                                         values.get("defaultValue") != null
219                                                                 ? values.get("defaultValue")
220                                                                         .toString()
221                                                                 : "",
222                                                         "fqName",
223                                                         nvl(fqName, ""),
224                                                         "description",
225                                                         desc,
226                                                         "since",
227                                                         nvl(since, ""),
228                                                         "configurationSource",
229                                                         source,
230                                                         "configurationType",
231                                                         type));
232                                             }
233                                         };
234                                     }
235                                     return null;
236                                 }
237                             };
238                         }
239                     },
240                     0);
241         } catch (IOException e) {
242             throw new RuntimeException(e);
243         }
244     }
245 
246     protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
247         JavaType<?> type = parse(path);
248         if (type instanceof JavaClassSource javaClassSource) {
249             javaClassSource.getFields().stream()
250                     .filter(this::hasConfigurationSource)
251                     .forEach(f -> {
252                         Map<String, String> constants = extractConstants(Paths.get(path.toString()
253                                 .replace("/src/main/java/", "/target/classes/")
254                                 .replace(".java", ".class")));
255 
256                         String name = f.getName();
257                         String key = constants.get(name);
258                         String fqName = f.getOrigin().getCanonicalName() + "." + name;
259                         String configurationType = getConfigurationType(f);
260                         String defValue = getTag(f, "@configurationDefaultValue");
261                         if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
262                             // constant "lookup"
263                             String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
264                             if (lookupValue == null) {
265                                 // currently we hard fail if javadoc cannot be looked up
266                                 // workaround: at cost of redundancy, but declare constants in situ for now
267                                 // (in same class)
268                                 throw new IllegalArgumentException(
269                                         "Could not look up " + defValue + " for configuration " + fqName);
270                             }
271                             defValue = lookupValue;
272                             if ("java.lang.Long".equals(configurationType)
273                                     && (defValue.endsWith("l") || defValue.endsWith("L"))) {
274                                 defValue = defValue.substring(0, defValue.length() - 1);
275                             }
276                         }
277                         discoveredKeys.add(Map.of(
278                                 KEY,
279                                 key,
280                                 "defaultValue",
281                                 nvl(defValue, ""),
282                                 "fqName",
283                                 fqName,
284                                 "description",
285                                 cleanseJavadoc(f),
286                                 "since",
287                                 nvl(getSince(f), ""),
288                                 "configurationSource",
289                                 getConfigurationSource(f),
290                                 "configurationType",
291                                 configurationType,
292                                 "supportRepoIdSuffix",
293                                 toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
294                     });
295         }
296     }
297 
298     protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
299         Javadoc jd = (Javadoc) javaDoc.getInternal();
300         return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
301                 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
302     }
303 
304     protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
305         JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
306         String[] text = javaDoc.getFullText().split("\n");
307         StringBuilder result = new StringBuilder();
308         for (String line : text) {
309             if (!line.startsWith("@") && !line.trim().isEmpty()) {
310                 result.append(line);
311             }
312         }
313         return cleanseTags(result.toString());
314     }
315 
316     protected String cleanseTags(String text) {
317         // {@code XXX} -> <pre>XXX</pre>
318         // {@link XXX} -> ??? pre for now
319         Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
320         Matcher matcher = pattern.matcher(text);
321         if (!matcher.find()) {
322             return text;
323         }
324         int prevEnd = 0;
325         StringBuilder result = new StringBuilder();
326         do {
327             result.append(text, prevEnd, matcher.start(1));
328             result.append("<code>");
329             result.append(matcher.group(2));
330             result.append("</code>");
331             prevEnd = matcher.end(1);
332         } while (matcher.find());
333         result.append(text, prevEnd, text.length());
334         return result.toString();
335     }
336 
337     protected JavaType<?> parse(Path path) {
338         try {
339             return Roaster.parse(path.toFile());
340         } catch (IOException e) {
341             throw new UncheckedIOException(e);
342         }
343     }
344 
345     protected String toYesNo(String value) {
346         return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
347     }
348 
349     protected String nvl(String string, String def) {
350         return string == null ? def : string;
351     }
352 
353     protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
354         return getTag(javaDocCapable, "@configurationSource") != null;
355     }
356 
357     protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
358         String type = getTag(javaDocCapable, "@configurationType");
359         if (type != null) {
360             String linkPrefix = "{@link ";
361             String linkSuffix = "}";
362             if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
363                 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
364             }
365             String javaLangPackage = "java.lang.";
366             if (type.startsWith(javaLangPackage)) {
367                 type = type.substring(javaLangPackage.length());
368             }
369         }
370         return nvl(type, "n/a");
371     }
372 
373     protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
374         String source = getTag(javaDocCapable, "@configurationSource");
375         if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
376             return "Session Configuration";
377         } else if ("{@link System#getProperty(String,String)}".equals(source)) {
378             return "Java System Properties";
379         } else {
380             return source;
381         }
382     }
383 
384     protected String getSince(JavaDocCapable<?> javaDocCapable) {
385         List<JavaDocTag> tags;
386         if (javaDocCapable != null) {
387             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
388                 tags = fieldSource.getJavaDoc().getTags("@since");
389                 if (tags.isEmpty()) {
390                     return getSince(fieldSource.getOrigin());
391                 } else {
392                     return tags.get(0).getValue();
393                 }
394             } else if (javaDocCapable instanceof JavaClassSource classSource) {
395                 tags = classSource.getJavaDoc().getTags("@since");
396                 if (!tags.isEmpty()) {
397                     return tags.get(0).getValue();
398                 }
399             }
400         }
401         return null;
402     }
403 
404     protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
405         List<JavaDocTag> tags;
406         if (javaDocCapable != null) {
407             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
408                 tags = fieldSource.getJavaDoc().getTags(tagName);
409                 if (tags.isEmpty()) {
410                     return getTag(fieldSource.getOrigin(), tagName);
411                 } else {
412                     return tags.get(0).getValue();
413                 }
414             }
415         }
416         return null;
417     }
418 
419     protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
420 
421     protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
422 
423     /**
424      * Builds "constant table" for one single class.
425      * <p>
426      * Limitations:
427      * - works only for single class (no inherited constants)
428      * - does not work for fields that are Enum.name()
429      * - more to come
430      */
431     protected static Map<String, String> extractConstants(Path file) {
432         StringWriter out = new StringWriter();
433         JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
434         Map<String, String> result = new HashMap<>();
435         out.getBuffer().toString().lines().forEach(l -> {
436             Matcher matcher = CONSTANT_PATTERN.matcher(l);
437             if (matcher.matches()) {
438                 result.put(matcher.group(1), matcher.group(2));
439             }
440         });
441         return result;
442     }
443 }