001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.tools; 020 021import java.io.IOException; 022import java.io.PrintWriter; 023import java.io.StringWriter; 024import java.io.UncheckedIOException; 025import java.io.Writer; 026import java.nio.charset.StandardCharsets; 027import java.nio.file.Files; 028import java.nio.file.Path; 029import java.nio.file.Paths; 030import java.util.ArrayList; 031import java.util.Comparator; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.Properties; 036import java.util.concurrent.Callable; 037import java.util.regex.Matcher; 038import java.util.regex.Pattern; 039import java.util.spi.ToolProvider; 040import java.util.stream.Stream; 041 042import org.apache.velocity.VelocityContext; 043import org.apache.velocity.app.VelocityEngine; 044import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; 045import org.codehaus.plexus.util.io.CachingWriter; 046import org.jboss.forge.roaster.Roaster; 047import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST; 048import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode; 049import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc; 050import org.jboss.forge.roaster.model.JavaDoc; 051import org.jboss.forge.roaster.model.JavaDocCapable; 052import org.jboss.forge.roaster.model.JavaDocTag; 053import org.jboss.forge.roaster.model.JavaType; 054import org.jboss.forge.roaster.model.impl.JavaDocImpl; 055import org.jboss.forge.roaster.model.source.FieldSource; 056import org.jboss.forge.roaster.model.source.JavaClassSource; 057import org.jboss.forge.roaster.model.source.JavaDocSource; 058import org.objectweb.asm.AnnotationVisitor; 059import org.objectweb.asm.ClassReader; 060import org.objectweb.asm.ClassVisitor; 061import org.objectweb.asm.FieldVisitor; 062import org.objectweb.asm.Opcodes; 063import picocli.CommandLine; 064 065@CommandLine.Command(name = "docgen", description = "Maven Documentation Generator") 066public class CollectConfiguration implements Callable<Integer> { 067 public static void main(String[] args) { 068 new CommandLine(new CollectConfiguration()).execute(args); 069 } 070 071 protected static final String KEY = "key"; 072 073 public enum Mode { 074 maven, 075 resolver 076 } 077 078 @CommandLine.Option( 079 names = {"-m", "--mode"}, 080 arity = "1", 081 paramLabel = "mode", 082 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'") 083 protected Mode mode; 084 085 @CommandLine.Option( 086 names = {"-t", "--templates"}, 087 arity = "1", 088 split = ",", 089 paramLabel = "template", 090 description = "The template names to write content out without '.vm' extension") 091 protected List<String> templates; 092 093 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from") 094 protected Path rootDirectory; 095 096 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to") 097 protected Path outputDirectory; 098 099 @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}