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.Collections;
32  import java.util.Comparator;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.Properties;
37  import java.util.concurrent.Callable;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  import java.util.spi.ToolProvider;
41  import java.util.stream.Stream;
42  
43  import org.apache.velocity.VelocityContext;
44  import org.apache.velocity.app.VelocityEngine;
45  import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
46  import org.codehaus.plexus.util.io.CachingWriter;
47  import org.jboss.forge.roaster.Roaster;
48  import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST;
49  import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode;
50  import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc;
51  import org.jboss.forge.roaster.model.JavaDoc;
52  import org.jboss.forge.roaster.model.JavaDocCapable;
53  import org.jboss.forge.roaster.model.JavaDocTag;
54  import org.jboss.forge.roaster.model.JavaType;
55  import org.jboss.forge.roaster.model.impl.JavaDocImpl;
56  import org.jboss.forge.roaster.model.source.FieldSource;
57  import org.jboss.forge.roaster.model.source.JavaClassSource;
58  import org.jboss.forge.roaster.model.source.JavaDocSource;
59  import org.objectweb.asm.AnnotationVisitor;
60  import org.objectweb.asm.ClassReader;
61  import org.objectweb.asm.ClassVisitor;
62  import org.objectweb.asm.FieldVisitor;
63  import org.objectweb.asm.Opcodes;
64  import picocli.CommandLine;
65  
66  @CommandLine.Command(name = "docgen", description = "Maven Documentation Generator")
67  public class CollectConfiguration implements Callable<Integer> {
68      public static void main(String[] args) {
69          new CommandLine(new CollectConfiguration()).execute(args);
70      }
71  
72      protected static final String KEY = "key";
73  
74      enum Mode {
75          maven,
76          resolver
77      }
78  
79      @CommandLine.Option(
80              names = {"-m", "--mode"},
81              arity = "1",
82              paramLabel = "mode",
83              description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'")
84      protected Mode mode;
85  
86      @CommandLine.Option(
87              names = {"-t", "--templates"},
88              arity = "1",
89              split = ",",
90              paramLabel = "template",
91              description = "The template names to write content out without '.vm' extension")
92      protected List<String> templates;
93  
94      @CommandLine.Parameters(index = "0", description = "The root directory to process sources from")
95      protected Path rootDirectory;
96  
97      @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to")
98      protected Path outputDirectory;
99  
100     @Override
101     public Integer call() {
102         try {
103             rootDirectory = rootDirectory.toAbsolutePath().normalize();
104             outputDirectory = outputDirectory.toAbsolutePath().normalize();
105 
106             ArrayList<Map<String, String>> discoveredKeys = new ArrayList<>();
107             try (Stream<Path> stream = Files.walk(rootDirectory)) {
108                 if (mode == Mode.maven) {
109                     System.out.println("Processing Maven sources from " + rootDirectory);
110                     stream.map(Path::toAbsolutePath)
111                             .filter(p -> p.getFileName().toString().endsWith(".class"))
112                             .filter(p -> p.toString().contains("/target/classes/"))
113                             .forEach(p -> {
114                                 processMavenClass(p, discoveredKeys);
115                             });
116                 } else if (mode == Mode.resolver) {
117                     System.out.println("Processing Resolver sources from " + rootDirectory);
118                     stream.map(Path::toAbsolutePath)
119                             .filter(p -> p.getFileName().toString().endsWith(".java"))
120                             .filter(p -> p.toString().contains("/src/main/java/"))
121                             .filter(p -> !p.toString().endsWith("/module-info.java"))
122                             .forEach(p -> processResolverClass(p, discoveredKeys));
123                 } else {
124                     throw new IllegalStateException("Unsupported mode " + mode);
125                 }
126             }
127 
128             Collections.sort(discoveredKeys, Comparator.comparing(e -> e.get(KEY)));
129 
130             Properties properties = new Properties();
131             properties.setProperty("resource.loaders", "classpath");
132             properties.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
133             VelocityEngine velocityEngine = new VelocityEngine();
134             velocityEngine.init(properties);
135 
136             VelocityContext context = new VelocityContext();
137             context.put("keys", discoveredKeys);
138 
139             for (String template : templates) {
140                 Path output = outputDirectory.resolve(template);
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 =
198                                                         switch ((values.get("source") != null
199                                                                         ? (String) values.get("source")
200                                                                         : "USER_PROPERTIES") // TODO: enum
201                                                                 .toLowerCase()) {
202                                                             case "model" -> "Model properties";
203                                                             case "user_properties" -> "User properties";
204                                                             default -> throw new IllegalStateException();
205                                                         };
206                                                 String type =
207                                                         switch ((values.get("type") != null
208                                                                 ? (String) values.get("type")
209                                                                 : "java.lang.String")) {
210                                                             case "java.lang.String" -> "String";
211                                                             case "java.lang.Integer" -> "Integer";
212                                                             case "java.lang.Boolean" -> "Boolean";
213                                                             default -> throw new IllegalStateException();
214                                                         };
215                                                 discoveredKeys.add(Map.of(
216                                                         KEY,
217                                                         fieldValue.toString(),
218                                                         "defaultValue",
219                                                         values.get("defaultValue") != null
220                                                                 ? values.get("defaultValue")
221                                                                         .toString()
222                                                                 : "",
223                                                         "fqName",
224                                                         nvl(fqName, ""),
225                                                         "description",
226                                                         desc,
227                                                         "since",
228                                                         nvl(since, ""),
229                                                         "configurationSource",
230                                                         source,
231                                                         "configurationType",
232                                                         type));
233                                             }
234                                         };
235                                     }
236                                     return null;
237                                 }
238                             };
239                         }
240                     },
241                     0);
242         } catch (IOException e) {
243             throw new RuntimeException(e);
244         }
245     }
246 
247     protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
248         JavaType<?> type = parse(path);
249         if (type instanceof JavaClassSource javaClassSource) {
250             javaClassSource.getFields().stream()
251                     .filter(this::hasConfigurationSource)
252                     .forEach(f -> {
253                         Map<String, String> constants = extractConstants(Paths.get(path.toString()
254                                 .replace("/src/main/java/", "/target/classes/")
255                                 .replace(".java", ".class")));
256 
257                         String name = f.getName();
258                         String key = constants.get(name);
259                         String fqName = f.getOrigin().getCanonicalName() + "." + name;
260                         String configurationType = getConfigurationType(f);
261                         String defValue = getTag(f, "@configurationDefaultValue");
262                         if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
263                             // constant "lookup"
264                             String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
265                             if (lookupValue == null) {
266                                 // currently we hard fail if javadoc cannot be looked up
267                                 // workaround: at cost of redundancy, but declare constants in situ for now
268                                 // (in same class)
269                                 throw new IllegalArgumentException(
270                                         "Could not look up " + defValue + " for configuration " + fqName);
271                             }
272                             defValue = lookupValue;
273                             if ("java.lang.Long".equals(configurationType)
274                                     && (defValue.endsWith("l") || defValue.endsWith("L"))) {
275                                 defValue = defValue.substring(0, defValue.length() - 1);
276                             }
277                         }
278                         discoveredKeys.add(Map.of(
279                                 KEY,
280                                 key,
281                                 "defaultValue",
282                                 nvl(defValue, ""),
283                                 "fqName",
284                                 fqName,
285                                 "description",
286                                 cleanseJavadoc(f),
287                                 "since",
288                                 nvl(getSince(f), ""),
289                                 "configurationSource",
290                                 getConfigurationSource(f),
291                                 "configurationType",
292                                 configurationType,
293                                 "supportRepoIdSuffix",
294                                 toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
295                     });
296         }
297     }
298 
299     protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
300         Javadoc jd = (Javadoc) javaDoc.getInternal();
301         return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
302                 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
303     }
304 
305     protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
306         JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
307         String[] text = javaDoc.getFullText().split("\n");
308         StringBuilder result = new StringBuilder();
309         for (String line : text) {
310             if (!line.startsWith("@") && !line.trim().isEmpty()) {
311                 result.append(line);
312             }
313         }
314         return cleanseTags(result.toString());
315     }
316 
317     protected String cleanseTags(String text) {
318         // {@code XXX} -> <pre>XXX</pre>
319         // {@link XXX} -> ??? pre for now
320         Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
321         Matcher matcher = pattern.matcher(text);
322         if (!matcher.find()) {
323             return text;
324         }
325         int prevEnd = 0;
326         StringBuilder result = new StringBuilder();
327         do {
328             result.append(text, prevEnd, matcher.start(1));
329             result.append("<code>");
330             result.append(matcher.group(2));
331             result.append("</code>");
332             prevEnd = matcher.end(1);
333         } while (matcher.find());
334         result.append(text, prevEnd, text.length());
335         return result.toString();
336     }
337 
338     protected JavaType<?> parse(Path path) {
339         try {
340             return Roaster.parse(path.toFile());
341         } catch (IOException e) {
342             throw new UncheckedIOException(e);
343         }
344     }
345 
346     protected String toYesNo(String value) {
347         return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
348     }
349 
350     protected String nvl(String string, String def) {
351         return string == null ? def : string;
352     }
353 
354     protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
355         return getTag(javaDocCapable, "@configurationSource") != null;
356     }
357 
358     protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
359         String type = getTag(javaDocCapable, "@configurationType");
360         if (type != null) {
361             String linkPrefix = "{@link ";
362             String linkSuffix = "}";
363             if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
364                 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
365             }
366             String javaLangPackage = "java.lang.";
367             if (type.startsWith(javaLangPackage)) {
368                 type = type.substring(javaLangPackage.length());
369             }
370         }
371         return nvl(type, "n/a");
372     }
373 
374     protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
375         String source = getTag(javaDocCapable, "@configurationSource");
376         if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
377             return "Session Configuration";
378         } else if ("{@link System#getProperty(String,String)}".equals(source)) {
379             return "Java System Properties";
380         } else {
381             return source;
382         }
383     }
384 
385     protected String getSince(JavaDocCapable<?> javaDocCapable) {
386         List<JavaDocTag> tags;
387         if (javaDocCapable != null) {
388             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
389                 tags = fieldSource.getJavaDoc().getTags("@since");
390                 if (tags.isEmpty()) {
391                     return getSince(fieldSource.getOrigin());
392                 } else {
393                     return tags.get(0).getValue();
394                 }
395             } else if (javaDocCapable instanceof JavaClassSource classSource) {
396                 tags = classSource.getJavaDoc().getTags("@since");
397                 if (!tags.isEmpty()) {
398                     return tags.get(0).getValue();
399                 }
400             }
401         }
402         return null;
403     }
404 
405     protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
406         List<JavaDocTag> tags;
407         if (javaDocCapable != null) {
408             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
409                 tags = fieldSource.getJavaDoc().getTags(tagName);
410                 if (tags.isEmpty()) {
411                     return getTag(fieldSource.getOrigin(), tagName);
412                 } else {
413                     return tags.get(0).getValue();
414                 }
415             }
416         }
417         return null;
418     }
419 
420     protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
421 
422     protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
423 
424     /**
425      * Builds "constant table" for one single class.
426      * <p>
427      * Limitations:
428      * - works only for single class (no inherited constants)
429      * - does not work for fields that are Enum.name()
430      * - more to come
431      */
432     protected static Map<String, String> extractConstants(Path file) {
433         StringWriter out = new StringWriter();
434         JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
435         Map<String, String> result = new HashMap<>();
436         out.getBuffer().toString().lines().forEach(l -> {
437             Matcher matcher = CONSTANT_PATTERN.matcher(l);
438             if (matcher.matches()) {
439                 result.put(matcher.group(1), matcher.group(2));
440             }
441         });
442         return result;
443     }
444 }