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 }; 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}