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