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.Collections; 032import java.util.Comparator; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.Properties; 037import java.util.concurrent.Callable; 038import java.util.regex.Matcher; 039import java.util.regex.Pattern; 040import java.util.spi.ToolProvider; 041import java.util.stream.Stream; 042 043import org.apache.velocity.VelocityContext; 044import org.apache.velocity.app.VelocityEngine; 045import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; 046import org.codehaus.plexus.util.io.CachingWriter; 047import org.jboss.forge.roaster.Roaster; 048import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.AST; 049import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ASTNode; 050import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Javadoc; 051import org.jboss.forge.roaster.model.JavaDoc; 052import org.jboss.forge.roaster.model.JavaDocCapable; 053import org.jboss.forge.roaster.model.JavaDocTag; 054import org.jboss.forge.roaster.model.JavaType; 055import org.jboss.forge.roaster.model.impl.JavaDocImpl; 056import org.jboss.forge.roaster.model.source.FieldSource; 057import org.jboss.forge.roaster.model.source.JavaClassSource; 058import org.jboss.forge.roaster.model.source.JavaDocSource; 059import org.objectweb.asm.AnnotationVisitor; 060import org.objectweb.asm.ClassReader; 061import org.objectweb.asm.ClassVisitor; 062import org.objectweb.asm.FieldVisitor; 063import org.objectweb.asm.Opcodes; 064import picocli.CommandLine; 065 066@CommandLine.Command(name = "docgen", description = "Maven Documentation Generator") 067public class CollectConfiguration implements Callable<Integer> { 068 public static void main(String[] args) { 069 new CommandLine(new CollectConfiguration()).execute(args); 070 } 071 072 protected static final String KEY = "key"; 073 074 enum Mode { 075 maven, 076 resolver 077 } 078 079 @CommandLine.Option( 080 names = {"-m", "--mode"}, 081 arity = "1", 082 paramLabel = "mode", 083 description = "The mode of generator (what is being scanned?), supported modes are 'maven', 'resolver'") 084 protected Mode mode; 085 086 @CommandLine.Option( 087 names = {"-t", "--templates"}, 088 arity = "1", 089 split = ",", 090 paramLabel = "template", 091 description = "The template names to write content out without '.vm' extension") 092 protected List<String> templates; 093 094 @CommandLine.Parameters(index = "0", description = "The root directory to process sources from") 095 protected Path rootDirectory; 096 097 @CommandLine.Parameters(index = "1", description = "The directory to generate output(s) to") 098 protected Path outputDirectory; 099 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}