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