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 javax.lang.model.SourceVersion; 022import javax.lang.model.element.AnnotationMirror; 023import javax.lang.model.element.AnnotationValue; 024import javax.lang.model.element.Element; 025import javax.lang.model.element.ElementKind; 026import javax.lang.model.element.ExecutableElement; 027import javax.lang.model.element.Modifier; 028import javax.lang.model.element.ModuleElement; 029import javax.lang.model.element.PackageElement; 030import javax.lang.model.element.TypeElement; 031import javax.lang.model.element.VariableElement; 032import javax.lang.model.type.DeclaredType; 033import javax.lang.model.type.PrimitiveType; 034import javax.lang.model.type.TypeMirror; 035import javax.lang.model.util.ElementFilter; 036import javax.lang.model.util.Elements; 037import javax.lang.model.util.SimpleElementVisitor14; 038import javax.lang.model.util.SimpleTypeVisitor14; 039import javax.lang.model.util.Types; 040import javax.tools.Diagnostic; 041 042import java.io.IOException; 043import java.io.PrintWriter; 044import java.io.Writer; 045import java.net.URI; 046import java.nio.charset.StandardCharsets; 047import java.nio.file.Files; 048import java.nio.file.InvalidPathException; 049import java.nio.file.Path; 050import java.nio.file.Paths; 051import java.util.ArrayList; 052import java.util.Arrays; 053import java.util.Collection; 054import java.util.Collections; 055import java.util.LinkedHashMap; 056import java.util.List; 057import java.util.Locale; 058import java.util.Map; 059import java.util.Objects; 060import java.util.Optional; 061import java.util.Properties; 062import java.util.Set; 063 064import com.sun.source.doctree.DeprecatedTree; 065import com.sun.source.doctree.DocCommentTree; 066import com.sun.source.doctree.DocTree; 067import com.sun.source.doctree.EntityTree; 068import com.sun.source.doctree.LinkTree; 069import com.sun.source.doctree.LiteralTree; 070import com.sun.source.doctree.ReferenceTree; 071import com.sun.source.doctree.SeeTree; 072import com.sun.source.doctree.SinceTree; 073import com.sun.source.doctree.SystemPropertyTree; 074import com.sun.source.doctree.TextTree; 075import com.sun.source.doctree.UnknownBlockTagTree; 076import com.sun.source.doctree.ValueTree; 077import com.sun.source.tree.ExpressionTree; 078import com.sun.source.tree.IdentifierTree; 079import com.sun.source.tree.MemberSelectTree; 080import com.sun.source.tree.VariableTree; 081import com.sun.source.util.DocTreePath; 082import com.sun.source.util.DocTrees; 083import com.sun.source.util.SimpleDocTreeVisitor; 084import jdk.javadoc.doclet.Doclet; 085import jdk.javadoc.doclet.DocletEnvironment; 086import jdk.javadoc.doclet.Reporter; 087import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference; 088import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType; 089import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 090 091/** 092 * A custom Javadoc {@link Doclet} that scans constant fields for configuration metadata declared via custom Javadoc 093 * block tags (e.g. {@code @configurationSource}) and writes the discovered keys into an intermediate 094 * {@link Properties} file. That file is subsequently consumed by {@link CollectConfiguration} to render the 095 * documentation via Velocity templates. 096 * <p> 097 * The intermediate file uses an indexed layout: 098 * <pre> 099 * keys.count=N 100 * keys.0.key=... 101 * keys.0.description=... 102 * ... 103 * </pre> 104 */ 105public class ConfigurationCollectorDoclet implements Doclet { 106 107 /** 108 * Fully qualified name of the Maven annotation that marks a configuration key when scanning Maven sources. 109 */ 110 private static final String MAVEN_CONFIG_ANNOTATION = "org.apache.maven.api.annotations.Config"; 111 112 private static final MethodReference METHOD_REFERENCE_SESSION_CONFIGURATION = 113 new MethodReference("org.eclipse.aether.RepositorySystemSession", "getConfigProperties", List.of()); 114 private static final MethodReference METHOD_REFERENCE_SYSTEM_PROPERTY = 115 new MethodReference("java.lang.System", "getProperty", List.of("java.lang.String", "java.lang.String")); 116 117 private Reporter reporter; 118 119 private Path output; 120 121 private URI internalJavadocUrl = URI.create("apidocs/"); 122 123 private String internalJavadocVersion = "21"; 124 125 private final List<URI> externalJavadocUrls = new ArrayList<>(); 126 127 private final List<Path> internalJavadocSourceRoots = new ArrayList<>(); 128 129 private enum Mode { 130 RESOLVER, 131 MAVEN 132 } 133 134 private record ConfigurationEntry( 135 String key, 136 String description, 137 String defaultValue, 138 String fqName, 139 String since, 140 String source, 141 String type, 142 String typeJavadocUrl, 143 boolean supportsRepoIdSuffix, 144 // is empty if not deprecated 145 String deprecated, 146 // see also section 147 Collection<String> see) { 148 149 public ConfigurationEntry { 150 Objects.requireNonNull(key); 151 Objects.requireNonNull(description); 152 } 153 } 154 155 private record ConfigurationType(String name, String javadocUrl) {} 156 157 /** 158 * The scanning mode; either {@code resolver} (Javadoc block tags) or {@code maven} (the {@code @Config} 159 * annotation). Defaults to {@code resolver}. 160 */ 161 private Mode mode = Mode.RESOLVER; 162 163 private DocTrees docTrees; 164 165 private Elements elements; 166 167 private Types types; 168 169 private JavadocLinkGenerator javadocLinkGenerator; 170 171 @Override 172 public void init(Locale locale, Reporter reporter) { 173 this.reporter = reporter; 174 } 175 176 @Override 177 public String getName() { 178 return "ConfigurationCollector"; 179 } 180 181 @Override 182 public Set<? extends Option> getSupportedOptions() { 183 return Set.of( 184 new SingleArgumentOption( 185 List.of("--output", "-o"), 186 "The intermediate properties file to write discovered keys to", 187 "<file>", 188 arg -> { 189 try { 190 output = Paths.get(arg); 191 } catch (InvalidPathException e) { 192 throw new IllegalArgumentException("Invalid output file path: " + arg, e); 193 } 194 }), 195 new SingleArgumentOption( 196 List.of("--mode", "-m"), "The scanning mode, either 'resolver' or 'maven'", "<mode>", arg -> { 197 try { 198 mode = Mode.valueOf(arg.toUpperCase(Locale.ROOT)); 199 } catch (IllegalArgumentException e) { 200 throw new IllegalArgumentException( 201 "Invalid mode: " + arg + ". Must be one of (case-insensitive): " 202 + String.join( 203 ", ", 204 Arrays.stream(Mode.values()) 205 .map(Enum::name) 206 .toArray(String[]::new))); 207 } 208 }), 209 new SingleArgumentOption( 210 List.of("--internal-javadoc-url"), 211 "The base URL for Javadoc generated by this project", 212 "<url>", 213 arg -> internalJavadocUrl = parseJavadocUrl(arg)), 214 new SingleArgumentOption( 215 List.of("--internal-javadoc-version"), 216 "The Javadoc tool version used to generate the internal site", 217 "<version>", 218 arg -> internalJavadocVersion = arg), 219 new SingleArgumentOption( 220 List.of("--internal-javadoc-source-root"), 221 "A source root whose public API is included in the internal Javadoc site", 222 "<directory>", 223 arg -> internalJavadocSourceRoots.add( 224 Paths.get(arg).toAbsolutePath().normalize())), 225 new SingleArgumentOption( 226 List.of("--external-javadoc-url"), 227 "An external Javadoc base URL used for validated links", 228 "<url>", 229 arg -> externalJavadocUrls.add(parseJavadocUrl(arg)))); 230 } 231 232 @Override 233 public SourceVersion getSupportedSourceVersion() { 234 return SourceVersion.latest(); 235 } 236 237 @Override 238 public boolean run(DocletEnvironment environment) { 239 try { 240 return doRun(environment); 241 } catch (RuntimeException e) { 242 // catch all runtime exception, as the default javadoc tool emits a confusing message about reporting 243 // something with Oracle 244 reportError("Error running ConfigurationCollectorDoclet", e); 245 return false; 246 } 247 } 248 249 private boolean doRun(DocletEnvironment environment) { 250 if (output == null) { 251 reportError("Missing required --output option"); 252 return false; 253 } 254 docTrees = environment.getDocTrees(); 255 elements = environment.getElementUtils(); 256 types = environment.getTypeUtils(); 257 javadocLinkGenerator = 258 new JavadocLinkGenerator(internalJavadocUrl, internalJavadocVersion, externalJavadocUrls, null); 259 List<ConfigurationEntry> configurationEntries = new ArrayList<>(); 260 261 Set<TypeElement> includedTypes = ElementFilter.typesIn(environment.getIncludedElements()); 262 for (TypeElement type : includedTypes) { 263 for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) { 264 // check if relevant metadata is present before processing the field, so that we can skip any fields 265 // that don't have a constant value or Javadoc 266 if (field.getConstantValue() == null) { 267 continue; 268 } 269 DocCommentTree docComment = docTrees.getDocCommentTree(field); 270 if (docComment == null) { 271 // javadoc is mandatory for configuration keys, so skip any fields that don't have a doc comment 272 continue; 273 } 274 DocTreePath rootPath = new DocTreePath(docTrees.getPath(field), docComment); 275 try { 276 ConfigurationEntry entry; 277 switch (mode) { 278 case MAVEN: 279 entry = processMavenField(rootPath, field); 280 break; 281 case RESOLVER: 282 entry = processResolverField(rootPath, field); 283 break; 284 default: 285 throw new IllegalStateException("Unknown mode: " + mode); 286 } 287 if (entry != null) { 288 configurationEntries.add(entry); 289 } 290 } catch (DocTreePathAwareRuntimeException e) { 291 reportError(e.getDocTreePath(), e.getMessage()); 292 } catch (IllegalArgumentException e) { 293 reportError(rootPath, e.getMessage()); 294 } catch (RuntimeException e) { 295 // log with stacktrace for unexpected errors, but continue 296 reportError(rootPath, e); 297 } 298 } 299 } 300 301 try { 302 writeProperties(configurationEntries); 303 } catch (IOException e) { 304 reportError("Failed to write properties file: " + e.getMessage()); 305 return false; 306 } 307 return true; 308 } 309 310 /** 311 * Reports an error message at a specific DocTreePath location. 312 * 313 * @param path the DocTreePath where the error occurred 314 * @param message the error message 315 */ 316 private void reportError(DocTreePath path, Throwable throwable) { 317 reportError(path, throwable.getMessage()); 318 reportError(throwable); 319 } 320 321 private void reportError(Throwable throwable) { 322 // also emit stack trace 323 PrintWriter pw = reporter.getDiagnosticWriter(); 324 if (pw == null) { 325 pw = new PrintWriter(System.err); 326 } 327 throwable.printStackTrace(pw); 328 } 329 330 /** 331 * Reports an error message at a specific DocTreePath location. 332 * 333 * @param path the DocTreePath where the error occurred 334 * @param message the error message 335 */ 336 private void reportError(DocTreePath path, String message) { 337 if (path != null) { 338 reporter.print(Diagnostic.Kind.ERROR, path, message); 339 } else { 340 reportError(message); 341 } 342 } 343 344 /** 345 * Reports a global error message without location information. 346 * 347 * @param message the error message 348 * @param throwable the exception whose stack trace is printed 349 */ 350 private void reportError(String message, Throwable throwable) { 351 reporter.print(Diagnostic.Kind.ERROR, message); 352 reportError(throwable); 353 } 354 355 /** 356 * Reports a global error message without location information. 357 * 358 * @param message the error message 359 */ 360 private void reportError(String message) { 361 reporter.print(Diagnostic.Kind.ERROR, message); 362 } 363 364 /** 365 * Processes a configuration key field declared in Javadoc sources. 366 * @param path 367 * @param field 368 * @return the extracted configuration entry (or {@code null}) 369 */ 370 private ConfigurationEntry processResolverField(DocTreePath path, VariableElement field) { 371 Objects.requireNonNull(path); 372 Objects.requireNonNull(field); 373 Map<String, UnknownBlockTagTree> blockTags = collectBlockTags(path.getDocComment()); 374 if (!blockTags.containsKey("configurationSource")) { 375 return null; 376 } 377 ConfigurationType configurationType = getConfigurationType(path, blockTags); 378 return new ConfigurationEntry( 379 String.valueOf(field.getConstantValue()), 380 getFullBodyContent(path), 381 resolveDefaultValue(path, blockTags).orElse(""), 382 getFullyQualifiedName(field), 383 getSince(path).orElse(""), 384 getConfigurationSource(path, blockTags).orElse(""), 385 configurationType.name(), 386 configurationType.javadocUrl(), 387 isSupportsRepoIdSuffix(path, blockTags), 388 getDeprecated(path, field).orElse(""), 389 getSee(path)); 390 } 391 392 private Optional<String> getDeprecated(DocTreePath path, Element element) { 393 Objects.requireNonNull(path, "path must not be null"); 394 Objects.requireNonNull(element, "field must not be null"); 395 396 // first check for deprecated annotation 397 if (element.getAnnotation(Deprecated.class) == null) { 398 // if not existing check enclosing elements recursively 399 return getDeprecated(element.getEnclosingElement()); 400 } 401 Optional<? extends DocTree> deprecatedTag = path.getDocComment().getBlockTags().stream() 402 .filter(t -> com.sun.source.doctree.DocTree.Kind.DEPRECATED == t.getKind()) 403 .findFirst(); 404 if (deprecatedTag.isPresent()) { 405 return Optional.of(renderContent(DocTreePath.getPath(path, deprecatedTag.get()), RenderMode.HTML, true)); 406 } 407 return Optional.of(""); 408 } 409 410 private Optional<String> getDeprecated(Element element) { 411 if (element == null) { 412 return Optional.empty(); 413 } 414 DocCommentTree docCommentTree = docTrees.getDocCommentTree(element); 415 if (docCommentTree == null) { 416 if (element.getAnnotation(Deprecated.class) != null) { 417 return Optional.of(""); 418 } 419 // traverse to enclosing element 420 return getDeprecated(element.getEnclosingElement()); 421 } else { 422 return getDeprecated(new DocTreePath(docTrees.getPath(element), docCommentTree), element); 423 } 424 } 425 426 private Collection<String> getSee(DocTreePath path) { 427 Objects.requireNonNull(path, "path must not be null"); 428 429 return path.getDocComment().getBlockTags().stream() 430 .filter(t -> com.sun.source.doctree.DocTree.Kind.SEE == t.getKind()) 431 .map(seeTag -> renderContent(DocTreePath.getPath(path, seeTag), RenderMode.HTML, true)) 432 .toList(); 433 } 434 435 private boolean isSupportsRepoIdSuffix(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) { 436 UnknownBlockTagTree repoIdTag = blockTags.get("configurationRepoIdSuffix"); 437 if (repoIdTag != null) { 438 String content = renderContent(DocTreePath.getPath(path, repoIdTag), RenderMode.PLAIN, true); 439 return "yes".equalsIgnoreCase(content) || "true".equalsIgnoreCase(content); 440 } 441 return false; 442 } 443 444 /** 445 * Processes a constant field declared in Maven sources. Maven declares configuration keys via the 446 * {@code org.apache.maven.api.annotations.Config} annotation (rather than the custom Javadoc block tags used by 447 * Resolver), so the metadata is read from that annotation's attributes. 448 * @return the extracted configuration entry (or {@code null} if the field is not annotated with {@code @Config}) 449 */ 450 // TODO: move to Maven repository module and use the Maven annotation type directly (currently we don't have a 451 // dependency on Maven API) 452 private ConfigurationEntry processMavenField(DocTreePath path, VariableElement field) { 453 AnnotationMirror config = getAnnotation(field, MAVEN_CONFIG_ANNOTATION); 454 if (config == null) { 455 return null; 456 } 457 458 String source = "USER_PROPERTIES"; 459 String defaultValue = ""; 460 String configurationType = "java.lang.String"; 461 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> attribute : 462 config.getElementValues().entrySet()) { 463 String name = attribute.getKey().getSimpleName().toString(); 464 Object value = attribute.getValue().getValue(); 465 switch (name) { 466 case "source": 467 source = value instanceof VariableElement variableElement 468 ? variableElement.getSimpleName().toString() 469 : String.valueOf(value); 470 break; 471 case "defaultValue": 472 defaultValue = String.valueOf(value); 473 break; 474 case "type": 475 configurationType = String.valueOf(value); 476 break; 477 default: 478 break; 479 } 480 } 481 482 source = source.toLowerCase(Locale.ROOT); 483 switch (source) { 484 case "model": 485 source = "Model properties"; 486 break; 487 case "user_properties": 488 source = "User properties"; 489 break; 490 case "system_properties": 491 source = "System properties"; 492 break; 493 default: 494 break; 495 } 496 497 if (configurationType.startsWith("java.lang.")) { 498 configurationType = configurationType.substring("java.lang.".length()); 499 } else if (configurationType.startsWith("java.util.")) { 500 configurationType = configurationType.substring("java.util.".length()); 501 } 502 return new ConfigurationEntry( 503 String.valueOf(field.getConstantValue()), 504 path.getDocComment() != null ? getFullBodyContent(path) : "", 505 Objects.toString(defaultValue, ""), 506 getFullyQualifiedName(field), 507 getSince(path).orElse(""), 508 source, 509 configurationType, 510 "", 511 false, 512 getDeprecated(path, field).orElse(""), 513 getSee(path)); 514 } 515 516 private AnnotationMirror getAnnotation(Element element, String fqName) { 517 for (AnnotationMirror annotation : element.getAnnotationMirrors()) { 518 Element annotationElement = annotation.getAnnotationType().asElement(); 519 if (annotationElement instanceof TypeElement 520 && ((TypeElement) annotationElement).getQualifiedName().contentEquals(fqName)) { 521 return annotation; 522 } 523 } 524 return null; 525 } 526 527 private void writeProperties(List<ConfigurationEntry> configurationEntries) throws IOException { 528 Properties properties = new Properties(); 529 properties.setProperty("keys.count", String.valueOf(configurationEntries.size())); 530 for (int i = 0; i < configurationEntries.size(); i++) { 531 ConfigurationEntry entry = configurationEntries.get(i); 532 writeEntry(properties, entry, "keys." + i + "."); 533 } 534 if (output.getParent() != null) { 535 Files.createDirectories(output.getParent()); 536 } 537 try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) { 538 properties.store(writer, "Generated by ConfigurationCollectorDoclet - DO NOT EDIT"); 539 } 540 } 541 542 private void writeEntry(Properties properties, ConfigurationEntry entry, String prefix) { 543 properties.setProperty(prefix + "key", entry.key()); 544 properties.setProperty(prefix + "defaultValue", entry.defaultValue()); 545 properties.setProperty(prefix + "fqName", entry.fqName()); 546 properties.setProperty(prefix + "description", entry.description()); 547 properties.setProperty(prefix + "since", entry.since()); 548 properties.setProperty(prefix + "configurationSource", entry.source()); 549 properties.setProperty(prefix + "configurationType", entry.type()); 550 properties.setProperty(prefix + "configurationTypeJavadocUrl", entry.typeJavadocUrl()); 551 properties.setProperty(prefix + "supportRepoIdSuffix", toYesNo(entry.supportsRepoIdSuffix())); 552 properties.setProperty(prefix + "deprecated", entry.deprecated()); 553 properties.setProperty(prefix + "see", String.join(", ", entry.see())); 554 } 555 556 // --- Javadoc extraction helpers ------------------------------------------------------------------------------- 557 558 private Map<String, UnknownBlockTagTree> collectBlockTags(DocCommentTree docComment) { 559 Map<String, UnknownBlockTagTree> result = new LinkedHashMap<>(); 560 if (docComment == null) { 561 return result; 562 } 563 for (DocTree tag : docComment.getBlockTags()) { 564 if (tag instanceof UnknownBlockTagTree unknownBlockTree) { 565 result.put(unknownBlockTree.getTagName(), unknownBlockTree); 566 } 567 } 568 return result; 569 } 570 571 private String getFullBodyContent(DocTreePath path) { 572 return renderContent(path, RenderMode.HTML, true, path.getDocComment().getFullBody()); 573 } 574 575 private Optional<String> resolveDefaultValue(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) { 576 UnknownBlockTagTree defaultValueTag = blockTags.get("configurationDefaultValue"); 577 if (defaultValueTag == null) { 578 return Optional.empty(); 579 } 580 DocTreePath defaultValuePath = DocTreePath.getPath(path, defaultValueTag); 581 for (DocTree tree : defaultValueTag.getContent()) { 582 if (tree instanceof LinkTree link) { 583 String signature = link.getReference().getSignature(); 584 DocTreePath linkTreePath = DocTreePath.getPath(path, tree); 585 // resolve the referenced constant using the fully qualified signature, so that references 586 // to constants declared in other types (e.g. {@link OtherType#CONSTANT}) can be resolved 587 VariableElement referenced = resolveReferencedField(linkTreePath, link.getReference()); 588 String value = referenced != null ? lookupConstant(referenced) : null; 589 if (value == null) { 590 // hard fail: default value constants must be resolvable; report at the precise 591 // link-reference location if we can resolve a path to it, otherwise at the block tag 592 DocTreePath linkRefPath = DocTreePath.getPath(linkTreePath, link.getReference()); 593 throw new DocTreePathAwareRuntimeException( 594 linkRefPath != null ? linkRefPath : linkTreePath, 595 "Could not resolve link to determine default value: " + signature); 596 } 597 return Optional.ofNullable(value); 598 } 599 } 600 // fallback: render the content of the block tag as-is (e.g. if it contains a literal value rather than a {@code 601 // {@link ...}} reference) 602 return Optional.of(renderContent(defaultValuePath, RenderMode.PLAIN, true)); 603 } 604 605 /** 606 * Resolves the {@link VariableElement} a {@code {@link ...}} reference points to using the fully qualified 607 * signature (so references into other types are supported). Returns {@code null} if the reference cannot be 608 * resolved to a field. 609 */ 610 private VariableElement resolveReferencedField(DocTreePath path, ReferenceTree reference) { 611 DocTreePath refPath = DocTreePath.getPath(path, reference); 612 if (refPath == null) { 613 return null; 614 } 615 Element element = docTrees.getElement(refPath); 616 return element instanceof VariableElement variableElement ? variableElement : null; 617 } 618 619 private String lookupConstant(VariableElement field) { 620 if (field.getConstantValue() != null) { 621 Object value = field.getConstantValue(); 622 if (value instanceof String) { 623 return "\"" + value + "\""; 624 } else { 625 return String.valueOf(field.getConstantValue()); 626 } 627 } 628 // enum constants don't expose a constant value, fall back to the enum value's name 629 if (field.getKind() == ElementKind.ENUM_CONSTANT) { 630 return field.getSimpleName().toString(); 631 } 632 // the field may indirectly reference an enum variable, e.g. "SomeEnum.VALUE"; 633 // resolve it from the field's initializer 634 return resolveEnumReference(field); 635 } 636 637 /** 638 * Resolves an enum constant that a field is initialized with, including the enum type in the result 639 * (e.g. a field declared as {@code SomeEnum FOO = SomeEnum.VALUE} resolves to {@code SomeEnum.VALUE}). 640 * Returns {@code null} if the field's initializer is not a simple enum reference. 641 */ 642 private String resolveEnumReference(VariableElement field) { 643 if (!(docTrees.getTree(field) instanceof VariableTree variableTree)) { 644 return null; 645 } 646 ExpressionTree initializer = variableTree.getInitializer(); 647 String enumConstant = null; 648 if (initializer instanceof MemberSelectTree memberSelectTree) { 649 // e.g. SomeEnum.VALUE -> VALUE 650 enumConstant = memberSelectTree.getIdentifier().toString(); 651 } else if (initializer instanceof IdentifierTree identifierTree) { 652 // e.g. statically imported VALUE -> VALUE 653 enumConstant = identifierTree.getName().toString(); 654 } 655 if (enumConstant == null) { 656 return null; 657 } 658 return enumConstant; 659 } 660 661 private Optional<LinkTree> getFirstLinkInBlockTag(UnknownBlockTagTree tag) { 662 for (DocTree tree : tag.getContent()) { 663 if (tree instanceof LinkTree link) { 664 return Optional.of(link); 665 } 666 } 667 return Optional.empty(); 668 } 669 670 /** 671 * Resolves the fully qualified type name a {@code {@link ...}} reference points to. 672 * @param path the path of the given inline link tag 673 * @param link the inline link tag 674 * @return 675 */ 676 private String getType(DocTreePath path, LinkTree link) { 677 String signature = link.getReference().getSignature(); 678 if (signature.contains("#")) { 679 // report at the precise link reference node within the block tag 680 DocTreePath linkRefPath = DocTreePath.getPath(path, link.getReference()); 681 throw new DocTreePathAwareRuntimeException( 682 linkRefPath != null ? linkRefPath : path, 683 "Expected a class link, but got a member reference: " + signature); 684 } 685 // resolve the referenced type and return its fully qualified name, falling back to the raw signature if it 686 // cannot be resolved 687 return resolveReferencedType(path, link.getReference()) 688 .map(t -> t.getQualifiedName().toString()) 689 .orElse(signature); 690 } 691 692 /** 693 * Resolves the fully qualified class name a {@code {@link ...}} class reference points to (so that simple names 694 * declared via imports are expanded). Falls back to the raw signature if the reference cannot be resolved to a 695 * type. 696 */ 697 private Optional<TypeElement> resolveReferencedType(DocTreePath path, ReferenceTree reference) { 698 // TODO: try to resolve from type outside the current compilation unit (e.g. from imports) 699 DocTreePath refPath = DocTreePath.getPath(path, reference); 700 if (refPath == null) { 701 return Optional.empty(); 702 } 703 Element element = docTrees.getElement(refPath); 704 return element instanceof TypeElement typeElement ? Optional.of(typeElement) : Optional.empty(); 705 } 706 707 enum RenderMode { 708 /** Render the content as plain text. Stripping any rich text markup */ 709 PLAIN, 710 /** Render the content as HTML, escaping special characters and rendering inline tags. */ 711 HTML 712 } 713 714 private String renderContent(DocTreePath docTreePath, RenderMode mode, boolean trim) { 715 return renderContent(docTreePath, mode, trim, null); 716 } 717 718 /** 719 * Renders the content of a Javadoc tag into an HTML string, escaping HTML special characters and rendering inline tags. 720 * 721 * @param docTreePath encapsulates the doc comment tree and the path to the content being rendered. 722 * The latter is used for resolving {@code {@link ...}} references and emitting error messages. 723 * @param trim if true, trims the result string (may destroy {@code <pre> </pre>} formatting). 724 * @param docTrees the doc trees for which to render the content. If {@code null}, the leaf of the {@code docTreePath} is rendered. 725 * @return the rendered content (never {@code null}) 726 * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/specs/javadoc/doc-comment-spec.html#standard-tags">Javadoc tags</a> 727 * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/api/jdk.compiler/com/sun/source/doctree/InlineTagTree.html">InlineTagTree (common superinterface of all inline tags)</a> 728 */ 729 private String renderContent( 730 DocTreePath docTreePath, RenderMode mode, boolean trim, Collection<? extends DocTree> docTreesToRender) { 731 Objects.requireNonNull(docTreePath, "docTreePath must not be null"); 732 StringBuilder sb = new StringBuilder(); 733 SimpleDocTreeVisitor<String, Void> visitor = new SimpleDocTreeVisitor<String, Void>() { 734 @Override 735 public String visitText(TextTree node, Void p) { 736 return escape(mode, node.getBody()); 737 } 738 739 @Override 740 public String visitLink(LinkTree node, Void p) { 741 List<? extends DocTree> labelTrees = node.getLabel(); 742 String label = (labelTrees != null && !labelTrees.isEmpty()) 743 ? renderContent(docTreePath, mode, false, labelTrees) 744 : ""; 745 return renderReference(node.getReference(), node.getKind() == DocTree.Kind.LINK_PLAIN, label); 746 } 747 748 @Override 749 public String visitLiteral(LiteralTree node, Void p) { 750 if (node.getKind() == DocTree.Kind.CODE) { 751 return renderAsCode(node.getBody().getBody()); 752 } else { 753 return escape(mode, node.getBody().getBody()); 754 } 755 } 756 757 @Override 758 public String visitSystemProperty(SystemPropertyTree node, Void p) { 759 return renderAsCode(node.getPropertyName().toString()); 760 } 761 762 private String renderAsCode(String text) { 763 if (mode == RenderMode.HTML) { 764 return "<code>" + escape(mode, text) + "</code>"; 765 } else { 766 return escape(mode, text); 767 } 768 } 769 770 @Override 771 public String visitValue(ValueTree node, Void p) { 772 if (node.getReference() != null) { 773 DocTreePath refPath = DocTreePath.getPath(docTreePath, node.getReference()); 774 if (refPath != null) { 775 Element element = docTrees.getElement(refPath); 776 if (element instanceof VariableElement ve) { 777 String value = lookupConstant(ve); 778 if (value != null) { 779 return renderAsCode(value); 780 } 781 } 782 } 783 } 784 // fall back to showing the reference signature 785 String ref = node.getReference() != null ? node.getReference().getSignature() : ""; 786 return renderAsCode(ref); 787 } 788 789 @Override 790 public String visitEntity(EntityTree node, Void p) { 791 return "&" + node.getName() + ";"; 792 } 793 794 @Override 795 public String visitUnknownBlockTag(UnknownBlockTagTree node, Void p) { 796 StringBuilder sb = new StringBuilder(); 797 node.getContent().forEach(child -> sb.append(child.accept(this, p))); 798 return sb.toString(); 799 } 800 801 @Override 802 public String visitSince(SinceTree node, Void p) { 803 return escape(mode, node.getBody().toString()); 804 } 805 806 @Override 807 public String visitDeprecated(DeprecatedTree node, Void p) { 808 StringBuilder sb = new StringBuilder(); 809 node.getBody().forEach(child -> sb.append(child.accept(this, p))); 810 return sb.toString(); 811 } 812 813 @Override 814 public String visitSee(SeeTree node, Void p) { 815 // if the first reference is of type REFERENCE, the label may follow afterwards 816 if (!node.getReference().isEmpty() && node.getReference().get(0) instanceof ReferenceTree reference) { 817 String label = renderContent( 818 docTreePath, 819 mode, 820 trim, 821 node.getReference().subList(1, node.getReference().size())); 822 return renderReference(reference, false, label); 823 } else { 824 return renderContent(docTreePath, mode, trim, node.getReference()); 825 } 826 } 827 828 /** 829 * Used from {@link #visitLink(LinkTree, Void)} and {@link #visitSee(SeeTree, Void)} to render a reference to a field or type (optionally with a label). 830 * @param node the reference node 831 * @param isPlain true if it should be emitted outside of a code block, false otherwise 832 * @param label may be empty, in which case the reference signature is used as the label 833 * @return the rendered reference (never {@code null}) 834 */ 835 private String renderReference(ReferenceTree node, boolean isPlain, String label) { 836 String effectiveLabel = label.isEmpty() ? node.getSignature() : label; 837 String escapedLabel = isPlain ? escape(mode, effectiveLabel) : renderAsCode(effectiveLabel); 838 if (mode == RenderMode.HTML) { 839 VariableElement referenced = resolveReferencedField(docTreePath, node); 840 String configurationKey = getConfigurationKey(referenced); 841 if (configurationKey != null) { 842 if (label.isEmpty()) { 843 // override the effective label with the configuration key 844 escapedLabel = isPlain ? escape(mode, configurationKey) : renderAsCode(configurationKey); 845 } 846 return "<a href=\"#" + escape(mode, configurationKey) + "\">" + escapedLabel + "</a>"; 847 } 848 Optional<String> javadocUrl = getJavadocUrl(docTreePath, node); 849 if (javadocUrl.isPresent()) { 850 return "<a href=\"" + javadocUrl.get() + "\">" + escapedLabel + "</a>"; 851 } 852 } 853 return escapedLabel; 854 } 855 856 @Override 857 protected String defaultAction(DocTree node, Void p) { 858 // the default action internally calls node.toString(), which uses 859 // com.sun.tools.javac.tree.DCTree.toString() which relies on com.sun.tools.javac.tree.DocPretty to 860 // render the node 861 return node.toString(); 862 } 863 }; 864 if (docTreesToRender == null) { 865 docTreesToRender = Collections.singleton(docTreePath.getLeaf()); 866 } 867 for (DocTree docTreeToRender : docTreesToRender) { 868 sb.append(docTreeToRender.accept(visitor, null)); 869 } 870 871 if (trim) { 872 // normalize whitespace not relevant for HTML rendering, 873 // trimming behaviour already differs between different Javadoc 874 // versions (Java > 21 trims leading whitespace per line) 875 return sb.toString().trim().replaceAll("\\s+", " "); 876 } else { 877 return sb.toString(); 878 } 879 } 880 881 private static String escape(RenderMode mode, String text) { 882 if (mode == RenderMode.HTML) { 883 return text.replace("&", "&").replace("<", "<").replace(">", ">"); 884 } else { 885 return text; 886 } 887 } 888 889 private Optional<String> getSince(DocTreePath path) { 890 String since = getSinceTag(path); 891 if (since == null && path.getTreePath().getParentPath() != null) { 892 // get the @since tag from the enclosing element (e.g. the enclosing class or package) 893 return getSince(docTrees.getElement(path.getTreePath().getParentPath())); 894 } 895 return Optional.ofNullable(since); 896 } 897 898 private Optional<String> getSince(Element element) { 899 if (element == null) { 900 return Optional.empty(); 901 } 902 DocCommentTree docComment = docTrees.getDocCommentTree(element); 903 if (docComment != null) { 904 DocTreePath path = new DocTreePath(docTrees.getPath(element), docComment); 905 Optional<String> since = getSince(path); 906 if (since.isPresent()) { 907 return since; 908 } 909 } 910 // traverse up the enclosing elements to find a @since tag in the closest enclosing type or package 911 return getSince(element.getEnclosingElement()); 912 } 913 914 private String getSinceTag(DocTreePath path) { 915 if (path == null) { 916 // may be non existent 917 return null; 918 } 919 for (DocTree tag : path.getDocComment().getBlockTags()) { 920 if (tag instanceof SinceTree) { 921 return renderContent(DocTreePath.getPath(path, tag), RenderMode.PLAIN, true); 922 } 923 } 924 return null; 925 } 926 927 private ConfigurationType getConfigurationType(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) { 928 UnknownBlockTagTree typeTag = blockTags.get("configurationType"); 929 if (typeTag == null) { 930 throw new IllegalStateException("Missing block tag @configurationType"); 931 } 932 DocTreePath configurationTypePath = DocTreePath.getPath(path, typeTag); 933 LinkTree linkTree = getFirstLinkInBlockTag(typeTag) 934 .orElseThrow(() -> new DocTreePathAwareRuntimeException( 935 configurationTypePath, "No valid {@link ...} reference found in @" + typeTag.getTagName())); 936 937 String type = getType(configurationTypePath, linkTree); 938 String javadocUrl = 939 getJavadocUrl(configurationTypePath, linkTree.getReference()).orElse(""); 940 String javaLangPackage = "java.lang."; 941 if (type.startsWith(javaLangPackage)) { 942 type = type.substring(javaLangPackage.length()); 943 } 944 return new ConfigurationType(type, javadocUrl); 945 } 946 947 private String getConfigurationKey(VariableElement field) { 948 if (field == null || !(field.getConstantValue() instanceof String key)) { 949 return null; 950 } 951 if (mode == Mode.MAVEN) { 952 return getAnnotation(field, MAVEN_CONFIG_ANNOTATION) != null ? key : null; 953 } 954 DocCommentTree docComment = docTrees.getDocCommentTree(field); 955 return docComment != null && collectBlockTags(docComment).containsKey("configurationSource") ? key : null; 956 } 957 958 private Optional<String> getJavadocUrl(DocTreePath path, ReferenceTree reference) { 959 if (reference == null) { 960 return Optional.empty(); 961 } 962 DocTreePath referencePath = DocTreePath.getPath(path, reference); 963 if (referencePath == null) { 964 return Optional.empty(); 965 } 966 Element element = docTrees.getElement(referencePath); 967 if (element == null) { 968 return Optional.empty(); 969 } 970 return createJavadocReference(element).flatMap(javadocReference -> { 971 try { 972 return Optional.of( 973 javadocLinkGenerator.createLink(javadocReference).toString()); 974 } catch (IllegalArgumentException e) { 975 return Optional.empty(); 976 } 977 }); 978 } 979 980 private Optional<FullyQualifiedJavadocReference> createJavadocReference(Element element) { 981 boolean external = !isInternalJavadocElement(element); 982 if (element instanceof ModuleElement moduleElement) { 983 return Optional.of(new FullyQualifiedJavadocReference( 984 Optional.of(moduleElement.getQualifiedName().toString()), 985 Optional.empty(), 986 Optional.empty(), 987 Optional.empty(), 988 Optional.empty(), 989 Optional.empty(), 990 external)); 991 } 992 993 Optional<String> moduleName = external ? Optional.empty() : getModuleName(element); 994 if (element instanceof PackageElement packageElement) { 995 return Optional.of(new FullyQualifiedJavadocReference( 996 moduleName, 997 Optional.of(packageElement.getQualifiedName().toString()), 998 Optional.empty(), 999 Optional.empty(), 1000 Optional.empty(), 1001 Optional.empty(), 1002 external)); 1003 } 1004 1005 TypeElement declaringType; 1006 Optional<String> member = Optional.empty(); 1007 Optional<MemberType> memberType = Optional.empty(); 1008 if (element instanceof TypeElement typeElement) { 1009 declaringType = typeElement; 1010 } else if (element instanceof VariableElement variableElement 1011 && variableElement.getEnclosingElement() instanceof TypeElement typeElement) { 1012 declaringType = typeElement; 1013 member = Optional.of(variableElement.getSimpleName().toString()); 1014 memberType = Optional.of(MemberType.FIELD); 1015 } else if (element instanceof ExecutableElement executableElement 1016 && executableElement.getEnclosingElement() instanceof TypeElement typeElement) { 1017 declaringType = typeElement; 1018 String executableName = executableElement.getKind() == ElementKind.CONSTRUCTOR 1019 ? typeElement.getSimpleName().toString() 1020 : executableElement.getSimpleName().toString(); 1021 String parameterTypes = String.join( 1022 ",", 1023 executableElement.getParameters().stream() 1024 .map(parameter -> getFullyQualifiedName(types.erasure(parameter.asType()))) 1025 .toList()); 1026 member = Optional.of(executableName + "(" + parameterTypes + ")"); 1027 memberType = Optional.of( 1028 executableElement.getKind() == ElementKind.CONSTRUCTOR 1029 ? MemberType.CONSTRUCTOR 1030 : MemberType.METHOD); 1031 } else { 1032 return Optional.empty(); 1033 } 1034 1035 PackageElement packageElement = elements.getPackageOf(declaringType); 1036 String packageName = packageElement.getQualifiedName().toString(); 1037 String qualifiedName = declaringType.getQualifiedName().toString(); 1038 String className = packageName.isEmpty() ? qualifiedName : qualifiedName.substring(packageName.length() + 1); 1039 return Optional.of(new FullyQualifiedJavadocReference( 1040 moduleName, 1041 Optional.of(packageName), 1042 Optional.of(className), 1043 member, 1044 memberType, 1045 Optional.empty(), 1046 external)); 1047 } 1048 1049 private Optional<String> getModuleName(Element element) { 1050 ModuleElement module = elements.getModuleOf(element); 1051 return module != null && !module.isUnnamed() 1052 ? Optional.of(module.getQualifiedName().toString()) 1053 : Optional.empty(); 1054 } 1055 1056 private boolean isInternalJavadocElement(Element element) { 1057 if (element instanceof ModuleElement) { 1058 return docTrees.getPath(element) != null; 1059 } 1060 if (element instanceof PackageElement packageElement) { 1061 return docTrees.getPath(element) != null || isPackageInInternalSourceTree(packageElement); 1062 } 1063 1064 TypeElement topLevelType = null; 1065 for (Element current = element; 1066 current != null && !(current instanceof PackageElement) && !(current instanceof ModuleElement); 1067 current = current.getEnclosingElement()) { 1068 if (current instanceof TypeElement typeElement) { 1069 topLevelType = typeElement; 1070 } 1071 if (isJavadocDeclaration(current) 1072 && !current.getModifiers().contains(Modifier.PUBLIC) 1073 && !current.getModifiers().contains(Modifier.PROTECTED)) { 1074 return false; 1075 } 1076 } 1077 return topLevelType != null 1078 && (docTrees.getPath(topLevelType) != null || isTypeInInternalSourceTree(topLevelType)); 1079 } 1080 1081 private boolean isPackageInInternalSourceTree(PackageElement packageElement) { 1082 Path packagePath = getPackagePath(packageElement); 1083 return internalJavadocSourceRoots.stream() 1084 .map(sourceRoot -> sourceRoot.resolve(packagePath)) 1085 .anyMatch(Files::isDirectory); 1086 } 1087 1088 private boolean isTypeInInternalSourceTree(TypeElement topLevelType) { 1089 Path packagePath = getPackagePath(elements.getPackageOf(topLevelType)); 1090 Path sourceFile = packagePath.resolve(topLevelType.getSimpleName() + ".java"); 1091 return internalJavadocSourceRoots.stream() 1092 .map(sourceRoot -> sourceRoot.resolve(sourceFile)) 1093 .anyMatch(Files::isRegularFile); 1094 } 1095 1096 private static Path getPackagePath(PackageElement packageElement) { 1097 String packageName = packageElement.getQualifiedName().toString(); 1098 return packageName.isEmpty() ? Path.of("") : Path.of(packageName.replace('.', '/')); 1099 } 1100 1101 private static boolean isJavadocDeclaration(Element element) { 1102 ElementKind kind = element.getKind(); 1103 return kind.isClass() 1104 || kind.isInterface() 1105 || kind == ElementKind.FIELD 1106 || kind == ElementKind.ENUM_CONSTANT 1107 || kind == ElementKind.METHOD 1108 || kind == ElementKind.CONSTRUCTOR; 1109 } 1110 1111 private Optional<String> getConfigurationSource(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) { 1112 UnknownBlockTagTree configurationSourceTag = blockTags.get("configurationSource"); 1113 if (configurationSourceTag == null) { 1114 return Optional.empty(); 1115 } 1116 DocTreePath configurationSourcePath = DocTreePath.getPath(path, configurationSourceTag); 1117 LinkTree linkTree = getFirstLinkInBlockTag(configurationSourceTag) 1118 .orElseThrow(() -> new DocTreePathAwareRuntimeException( 1119 configurationSourcePath, 1120 "No valid {@link ...} reference found in @" + configurationSourceTag.getTagName())); 1121 1122 // javadoc signature is not normalized, use the resolved reference (leveraging ReferenceParser) to get a unique 1123 // canonical representation of the referenced method 1124 MethodReference methodReference = getReferencedMethod(configurationSourcePath, linkTree); 1125 if (methodReference.equals(METHOD_REFERENCE_SESSION_CONFIGURATION)) { 1126 return Optional.of("Session Configuration"); 1127 } else if (methodReference.equals(METHOD_REFERENCE_SYSTEM_PROPERTY)) { 1128 return Optional.of("Java System Properties"); 1129 } else { 1130 reporter.print( 1131 Diagnostic.Kind.WARNING, 1132 path, 1133 "Unknown configuration source: " + linkTree.getReference().getSignature() 1134 + ", using raw signature as source"); 1135 return Optional.of(linkTree.getReference().getSignature()); 1136 } 1137 } 1138 1139 /** 1140 * Represents a reference to a method, including the fully qualified class name, method name, and parameter types. 1141 * This is supposed to be unique as well as canonical. 1142 * The signature within a Javadoc link is not normalized (e.g. may contain spaces or not, may contain argument names or not) 1143 * so we need to resolve the reference to get a unique representation of the method. 1144 * @param fullyQualifiedClassName the fully qualified name of the class containing the method 1145 * @param methodName the name of the method 1146 * @param fullyQualifiedParameterTypes a list of fully qualified names (for declared types) or simple names (for primitive types) of the parameter types of the method 1147 */ 1148 protected record MethodReference( 1149 String fullyQualifiedClassName, String methodName, List<String> fullyQualifiedParameterTypes) {} 1150 1151 private MethodReference getReferencedMethod(DocTreePath path, LinkTree link) { 1152 ExecutableElement ee = getReferencedExecutableElement(path, link); 1153 String fullyQualifiedClassName = 1154 ((TypeElement) ee.getEnclosingElement()).getQualifiedName().toString(); 1155 String methodName = ee.getSimpleName().toString(); 1156 List<String> parameterTypes = ee.getParameters().stream() 1157 .map(p -> getFullyQualifiedName(p.asType())) 1158 .toList(); 1159 return new MethodReference(fullyQualifiedClassName, methodName, parameterTypes); 1160 } 1161 1162 static String getFullyQualifiedName(Element e) { 1163 return new SimpleElementVisitor14<String, Void>() { 1164 @Override 1165 public String visitModule(ModuleElement e, Void p) { 1166 return e.getQualifiedName().toString(); 1167 } 1168 1169 @Override 1170 public String visitPackage(PackageElement e, Void p) { 1171 return e.getQualifiedName().toString(); 1172 } 1173 1174 @Override 1175 public String visitType(TypeElement e, Void p) { 1176 return e.getQualifiedName().toString(); 1177 } 1178 1179 @Override 1180 protected String defaultAction(Element e, Void p) { 1181 return visit(e.getEnclosingElement()) + "." + e.getSimpleName(); 1182 } 1183 }.visit(e); 1184 } 1185 1186 static String getFullyQualifiedName(TypeMirror e) { 1187 return new SimpleTypeVisitor14<String, Void>() { 1188 @Override 1189 public String visitDeclared(DeclaredType t, Void p) { 1190 Element e = t.asElement(); 1191 if (e instanceof TypeElement typeElement) { 1192 return typeElement.getQualifiedName().toString(); 1193 } 1194 return super.visitDeclared(t, p); 1195 } 1196 1197 @Override 1198 public String visitPrimitive(PrimitiveType t, Void p) { 1199 return t.toString(); 1200 } 1201 1202 @Override 1203 protected String defaultAction(TypeMirror e, Void p) { 1204 return e.toString(); 1205 } 1206 }.visit(e); 1207 } 1208 1209 private ExecutableElement getReferencedExecutableElement(DocTreePath path, LinkTree link) { 1210 DocTreePath linkRefPath = DocTreePath.getPath(path, link.getReference()); 1211 if (linkRefPath == null) { 1212 throw new DocTreePathAwareRuntimeException( 1213 path, 1214 "Could not resolve link reference: " + link.getReference().getSignature()); 1215 } 1216 Element element = docTrees.getElement(linkRefPath); 1217 if (element instanceof ExecutableElement ee) { 1218 return ee; 1219 } else { 1220 throw new DocTreePathAwareRuntimeException( 1221 linkRefPath, "Expected an executable element, but got: " + element); 1222 } 1223 } 1224 1225 private static String toYesNo(boolean value) { 1226 return value ? "Yes" : "No"; 1227 } 1228 1229 private static URI parseJavadocUrl(String value) { 1230 URI uri = URI.create(value); 1231 if (uri.getQuery() != null || uri.getFragment() != null) { 1232 throw new IllegalArgumentException("Javadoc base URL must not contain a query or fragment: " + value); 1233 } 1234 return value.endsWith("/") ? uri : URI.create(value + "/"); 1235 } 1236 1237 /** 1238 * Minimal {@link Option} implementation. 1239 */ 1240 private static final class SingleArgumentOption implements Option { 1241 private final List<String> names; 1242 private final String description; 1243 private final String parameters; 1244 private final java.util.function.Consumer<String> processor; 1245 1246 SingleArgumentOption( 1247 List<String> names, 1248 String description, 1249 String parameters, 1250 java.util.function.Consumer<String> processor) { 1251 this.names = names; 1252 this.description = description; 1253 this.parameters = parameters; 1254 this.processor = processor; 1255 } 1256 1257 @Override 1258 public int getArgumentCount() { 1259 return 1; 1260 } 1261 1262 @Override 1263 public String getDescription() { 1264 return description; 1265 } 1266 1267 @Override 1268 public Kind getKind() { 1269 return Kind.STANDARD; 1270 } 1271 1272 @Override 1273 public List<String> getNames() { 1274 return names; 1275 } 1276 1277 @Override 1278 public String getParameters() { 1279 return parameters; 1280 } 1281 1282 @Override 1283 public boolean process(String option, List<String> arguments) { 1284 processor.accept(arguments.get(0)); 1285 // returning false just leads to a very generic error message (not even exposing the affected option) so 1286 // rather rely on custom runtime exceptions for validation errors 1287 return true; 1288 } 1289 } 1290}