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                                                 };
205                                                 String type = (values.get("type") != null
206                                                         ? (String) values.get("type")
207                                                         : "java.lang.String");
208                                                 if (type.startsWith("java.lang.")) {
209                                                     type = type.substring("java.lang.".length());
210                                                 } else if (type.startsWith("java.util.")) {
211                                                     type = type.substring("java.util.".length());
212                                                 }
213                                                 discoveredKeys.add(Map.of(
214                                                         KEY,
215                                                         fieldValue.toString(),
216                                                         "defaultValue",
217                                                         values.get("defaultValue") != null
218                                                                 ? values.get("defaultValue")
219                                                                         .toString()
220                                                                 : "",
221                                                         "fqName",
222                                                         nvl(fqName, ""),
223                                                         "description",
224                                                         desc,
225                                                         "since",
226                                                         nvl(since, ""),
227                                                         "configurationSource",
228                                                         source,
229                                                         "configurationType",
230                                                         type));
231                                             }
232                                         };
233                                     }
234                                     return null;
235                                 }
236                             };
237                         }
238                     },
239                     0);
240         } catch (IOException e) {
241             throw new RuntimeException(e);
242         }
243     }
244 
245     protected void processResolverClass(Path path, List<Map<String, String>> discoveredKeys) {
246         JavaType<?> type = parse(path);
247         if (type instanceof JavaClassSource javaClassSource) {
248             javaClassSource.getFields().stream()
249                     .filter(this::hasConfigurationSource)
250                     .forEach(f -> {
251                         Map<String, String> constants = extractConstants(Paths.get(path.toString()
252                                 .replace("/src/main/java/", "/target/classes/")
253                                 .replace(".java", ".class")));
254 
255                         String name = f.getName();
256                         String key = constants.get(name);
257                         String fqName = f.getOrigin().getCanonicalName() + "." + name;
258                         String configurationType = getConfigurationType(f);
259                         String defValue = getTag(f, "@configurationDefaultValue");
260                         if (defValue != null && defValue.startsWith("{@link #") && defValue.endsWith("}")) {
261                             // constant "lookup"
262                             String lookupValue = constants.get(defValue.substring(8, defValue.length() - 1));
263                             if (lookupValue == null) {
264                                 // currently we hard fail if javadoc cannot be looked up
265                                 // workaround: at cost of redundancy, but declare constants in situ for now
266                                 // (in same class)
267                                 throw new IllegalArgumentException(
268                                         "Could not look up " + defValue + " for configuration " + fqName);
269                             }
270                             defValue = lookupValue;
271                             if ("java.lang.Long".equals(configurationType)
272                                     && (defValue.endsWith("l") || defValue.endsWith("L"))) {
273                                 defValue = defValue.substring(0, defValue.length() - 1);
274                             }
275                         }
276                         discoveredKeys.add(Map.of(
277                                 KEY,
278                                 key,
279                                 "defaultValue",
280                                 nvl(defValue, ""),
281                                 "fqName",
282                                 fqName,
283                                 "description",
284                                 cleanseJavadoc(f),
285                                 "since",
286                                 nvl(getSince(f), ""),
287                                 "configurationSource",
288                                 getConfigurationSource(f),
289                                 "configurationType",
290                                 configurationType,
291                                 "supportRepoIdSuffix",
292                                 toYesNo(getTag(f, "@configurationRepoIdSuffix"))));
293                     });
294         }
295     }
296 
297     protected JavaDocSource<Object> cloneJavadoc(JavaDocSource<?> javaDoc) {
298         Javadoc jd = (Javadoc) javaDoc.getInternal();
299         return new JavaDocImpl<>(javaDoc.getOrigin(), (Javadoc)
300                 ASTNode.copySubtree(AST.newAST(jd.getAST().apiLevel(), false), jd));
301     }
302 
303     protected String cleanseJavadoc(FieldSource<JavaClassSource> javaClassSource) {
304         JavaDoc<FieldSource<JavaClassSource>> javaDoc = javaClassSource.getJavaDoc();
305         String[] text = javaDoc.getFullText().split("\n");
306         StringBuilder result = new StringBuilder();
307         for (String line : text) {
308             if (!line.startsWith("@") && !line.trim().isEmpty()) {
309                 result.append(line);
310             }
311         }
312         return cleanseTags(result.toString());
313     }
314 
315     protected String cleanseTags(String text) {
316         // {@code XXX} -> <pre>XXX</pre>
317         // {@link XXX} -> ??? pre for now
318         Pattern pattern = Pattern.compile("(\\{@\\w\\w\\w\\w (.+?)})");
319         Matcher matcher = pattern.matcher(text);
320         if (!matcher.find()) {
321             return text;
322         }
323         int prevEnd = 0;
324         StringBuilder result = new StringBuilder();
325         do {
326             result.append(text, prevEnd, matcher.start(1));
327             result.append("<code>");
328             result.append(matcher.group(2));
329             result.append("</code>");
330             prevEnd = matcher.end(1);
331         } while (matcher.find());
332         result.append(text, prevEnd, text.length());
333         return result.toString();
334     }
335 
336     protected JavaType<?> parse(Path path) {
337         try {
338             return Roaster.parse(path.toFile());
339         } catch (IOException e) {
340             throw new UncheckedIOException(e);
341         }
342     }
343 
344     protected String toYesNo(String value) {
345         return "yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) ? "Yes" : "No";
346     }
347 
348     protected String nvl(String string, String def) {
349         return string == null ? def : string;
350     }
351 
352     protected boolean hasConfigurationSource(JavaDocCapable<?> javaDocCapable) {
353         return getTag(javaDocCapable, "@configurationSource") != null;
354     }
355 
356     protected String getConfigurationType(JavaDocCapable<?> javaDocCapable) {
357         String type = getTag(javaDocCapable, "@configurationType");
358         if (type != null) {
359             String linkPrefix = "{@link ";
360             String linkSuffix = "}";
361             if (type.startsWith(linkPrefix) && type.endsWith(linkSuffix)) {
362                 type = type.substring(linkPrefix.length(), type.length() - linkSuffix.length());
363             }
364             String javaLangPackage = "java.lang.";
365             if (type.startsWith(javaLangPackage)) {
366                 type = type.substring(javaLangPackage.length());
367             }
368         }
369         return nvl(type, "n/a");
370     }
371 
372     protected String getConfigurationSource(JavaDocCapable<?> javaDocCapable) {
373         String source = getTag(javaDocCapable, "@configurationSource");
374         if ("{@link RepositorySystemSession#getConfigProperties()}".equals(source)) {
375             return "Session Configuration";
376         } else if ("{@link System#getProperty(String,String)}".equals(source)) {
377             return "Java System Properties";
378         } else {
379             return source;
380         }
381     }
382 
383     protected String getSince(JavaDocCapable<?> javaDocCapable) {
384         List<JavaDocTag> tags;
385         if (javaDocCapable != null) {
386             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
387                 tags = fieldSource.getJavaDoc().getTags("@since");
388                 if (tags.isEmpty()) {
389                     return getSince(fieldSource.getOrigin());
390                 } else {
391                     return tags.get(0).getValue();
392                 }
393             } else if (javaDocCapable instanceof JavaClassSource classSource) {
394                 tags = classSource.getJavaDoc().getTags("@since");
395                 if (!tags.isEmpty()) {
396                     return tags.get(0).getValue();
397                 }
398             }
399         }
400         return null;
401     }
402 
403     protected String getTag(JavaDocCapable<?> javaDocCapable, String tagName) {
404         List<JavaDocTag> tags;
405         if (javaDocCapable != null) {
406             if (javaDocCapable instanceof FieldSource<?> fieldSource) {
407                 tags = fieldSource.getJavaDoc().getTags(tagName);
408                 if (tags.isEmpty()) {
409                     return getTag(fieldSource.getOrigin(), tagName);
410                 } else {
411                     return tags.get(0).getValue();
412                 }
413             }
414         }
415         return null;
416     }
417 
418     protected static final Pattern CONSTANT_PATTERN = Pattern.compile(".*static final.* ([A-Z_]+) = (.*);");
419 
420     protected static final ToolProvider JAVAP = ToolProvider.findFirst("javap").orElseThrow();
421 
422     /**
423      * Builds "constant table" for one single class.
424      * <p>
425      * Limitations:
426      * - works only for single class (no inherited constants)
427      * - does not work for fields that are Enum.name()
428      * - more to come
429      */
430     protected static Map<String, String> extractConstants(Path file) {
431         StringWriter out = new StringWriter();
432         JAVAP.run(new PrintWriter(out), new PrintWriter(System.err), "-constants", file.toString());
433         Map<String, String> result = new HashMap<>();
434         out.getBuffer().toString().lines().forEach(l -> {
435             Matcher matcher = CONSTANT_PATTERN.matcher(l);
436             if (matcher.matches()) {
437                 result.put(matcher.group(1), matcher.group(2));
438             }
439         });
440         return result;
441     }
442 }