1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.tools;
20
21 import javax.lang.model.SourceVersion;
22 import javax.lang.model.element.AnnotationMirror;
23 import javax.lang.model.element.AnnotationValue;
24 import javax.lang.model.element.Element;
25 import javax.lang.model.element.ElementKind;
26 import javax.lang.model.element.ExecutableElement;
27 import javax.lang.model.element.Modifier;
28 import javax.lang.model.element.ModuleElement;
29 import javax.lang.model.element.PackageElement;
30 import javax.lang.model.element.TypeElement;
31 import javax.lang.model.element.VariableElement;
32 import javax.lang.model.type.DeclaredType;
33 import javax.lang.model.type.PrimitiveType;
34 import javax.lang.model.type.TypeMirror;
35 import javax.lang.model.util.ElementFilter;
36 import javax.lang.model.util.Elements;
37 import javax.lang.model.util.SimpleElementVisitor14;
38 import javax.lang.model.util.SimpleTypeVisitor14;
39 import javax.lang.model.util.Types;
40 import javax.tools.Diagnostic;
41
42 import java.io.IOException;
43 import java.io.PrintWriter;
44 import java.io.Writer;
45 import java.net.URI;
46 import java.nio.charset.StandardCharsets;
47 import java.nio.file.Files;
48 import java.nio.file.InvalidPathException;
49 import java.nio.file.Path;
50 import java.nio.file.Paths;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.Collection;
54 import java.util.Collections;
55 import java.util.LinkedHashMap;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.Map;
59 import java.util.Objects;
60 import java.util.Optional;
61 import java.util.Properties;
62 import java.util.Set;
63
64 import com.sun.source.doctree.DeprecatedTree;
65 import com.sun.source.doctree.DocCommentTree;
66 import com.sun.source.doctree.DocTree;
67 import com.sun.source.doctree.EntityTree;
68 import com.sun.source.doctree.LinkTree;
69 import com.sun.source.doctree.LiteralTree;
70 import com.sun.source.doctree.ReferenceTree;
71 import com.sun.source.doctree.SinceTree;
72 import com.sun.source.doctree.SystemPropertyTree;
73 import com.sun.source.doctree.TextTree;
74 import com.sun.source.doctree.UnknownBlockTagTree;
75 import com.sun.source.doctree.ValueTree;
76 import com.sun.source.tree.ExpressionTree;
77 import com.sun.source.tree.IdentifierTree;
78 import com.sun.source.tree.MemberSelectTree;
79 import com.sun.source.tree.VariableTree;
80 import com.sun.source.util.DocTreePath;
81 import com.sun.source.util.DocTrees;
82 import com.sun.source.util.SimpleDocTreeVisitor;
83 import jdk.javadoc.doclet.Doclet;
84 import jdk.javadoc.doclet.DocletEnvironment;
85 import jdk.javadoc.doclet.Reporter;
86 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
87 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
88 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104 public class ConfigurationCollectorDoclet implements Doclet {
105
106
107
108
109 private static final String MAVEN_CONFIG_ANNOTATION = "org.apache.maven.api.annotations.Config";
110
111 private static final MethodReference METHOD_REFERENCE_SESSION_CONFIGURATION =
112 new MethodReference("org.eclipse.aether.RepositorySystemSession", "getConfigProperties", List.of());
113 private static final MethodReference METHOD_REFERENCE_SYSTEM_PROPERTY =
114 new MethodReference("java.lang.System", "getProperty", List.of("java.lang.String", "java.lang.String"));
115
116 private Reporter reporter;
117
118 private Path output;
119
120 private URI internalJavadocUrl = URI.create("apidocs/");
121
122 private String internalJavadocVersion = "21";
123
124 private final List<URI> externalJavadocUrls = new ArrayList<>();
125
126 private final List<Path> internalJavadocSourceRoots = new ArrayList<>();
127
128 private enum Mode {
129 RESOLVER,
130 MAVEN
131 }
132
133 private record ConfigurationEntry(
134 String key,
135 String description,
136 String defaultValue,
137 String fqName,
138 String since,
139 String source,
140 String type,
141 String typeJavadocUrl,
142 boolean supportsRepoIdSuffix,
143
144 String deprecated) {
145
146 public ConfigurationEntry {
147 Objects.requireNonNull(key);
148 Objects.requireNonNull(description);
149 }
150 }
151
152 private record ConfigurationType(String name, String javadocUrl) {}
153
154
155
156
157
158 private Mode mode = Mode.RESOLVER;
159
160 private DocTrees docTrees;
161
162 private Elements elements;
163
164 private Types types;
165
166 private JavadocLinkGenerator javadocLinkGenerator;
167
168 @Override
169 public void init(Locale locale, Reporter reporter) {
170 this.reporter = reporter;
171 }
172
173 @Override
174 public String getName() {
175 return "ConfigurationCollector";
176 }
177
178 @Override
179 public Set<? extends Option> getSupportedOptions() {
180 return Set.of(
181 new SingleArgumentOption(
182 List.of("--output", "-o"),
183 "The intermediate properties file to write discovered keys to",
184 "<file>",
185 arg -> {
186 try {
187 output = Paths.get(arg);
188 } catch (InvalidPathException e) {
189 throw new IllegalArgumentException("Invalid output file path: " + arg, e);
190 }
191 }),
192 new SingleArgumentOption(
193 List.of("--mode", "-m"), "The scanning mode, either 'resolver' or 'maven'", "<mode>", arg -> {
194 try {
195 mode = Mode.valueOf(arg.toUpperCase(Locale.ROOT));
196 } catch (IllegalArgumentException e) {
197 throw new IllegalArgumentException(
198 "Invalid mode: " + arg + ". Must be one of (case-insensitive): "
199 + String.join(
200 ", ",
201 Arrays.stream(Mode.values())
202 .map(Enum::name)
203 .toArray(String[]::new)));
204 }
205 }),
206 new SingleArgumentOption(
207 List.of("--internal-javadoc-url"),
208 "The base URL for Javadoc generated by this project",
209 "<url>",
210 arg -> internalJavadocUrl = parseJavadocUrl(arg)),
211 new SingleArgumentOption(
212 List.of("--internal-javadoc-version"),
213 "The Javadoc tool version used to generate the internal site",
214 "<version>",
215 arg -> internalJavadocVersion = arg),
216 new SingleArgumentOption(
217 List.of("--internal-javadoc-source-root"),
218 "A source root whose public API is included in the internal Javadoc site",
219 "<directory>",
220 arg -> internalJavadocSourceRoots.add(
221 Paths.get(arg).toAbsolutePath().normalize())),
222 new SingleArgumentOption(
223 List.of("--external-javadoc-url"),
224 "An external Javadoc base URL used for validated links",
225 "<url>",
226 arg -> externalJavadocUrls.add(parseJavadocUrl(arg))));
227 }
228
229 @Override
230 public SourceVersion getSupportedSourceVersion() {
231 return SourceVersion.latest();
232 }
233
234 @Override
235 public boolean run(DocletEnvironment environment) {
236 try {
237 return doRun(environment);
238 } catch (RuntimeException e) {
239
240
241 reportError("Error running ConfigurationCollectorDoclet", e);
242 return false;
243 }
244 }
245
246 private boolean doRun(DocletEnvironment environment) {
247 if (output == null) {
248 reportError("Missing required --output option");
249 return false;
250 }
251 docTrees = environment.getDocTrees();
252 elements = environment.getElementUtils();
253 types = environment.getTypeUtils();
254 javadocLinkGenerator =
255 new JavadocLinkGenerator(internalJavadocUrl, internalJavadocVersion, externalJavadocUrls, null);
256 List<ConfigurationEntry> configurationEntries = new ArrayList<>();
257
258 Set<TypeElement> includedTypes = ElementFilter.typesIn(environment.getIncludedElements());
259 for (TypeElement type : includedTypes) {
260 for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) {
261
262
263 if (field.getConstantValue() == null) {
264 continue;
265 }
266 DocCommentTree docComment = docTrees.getDocCommentTree(field);
267 if (docComment == null) {
268
269 continue;
270 }
271 DocTreePath rootPath = new DocTreePath(docTrees.getPath(field), docComment);
272 try {
273 ConfigurationEntry entry;
274 switch (mode) {
275 case MAVEN:
276 entry = processMavenField(rootPath, field);
277 break;
278 case RESOLVER:
279 entry = processResolverField(rootPath, field);
280 break;
281 default:
282 throw new IllegalStateException("Unknown mode: " + mode);
283 }
284 if (entry != null) {
285 configurationEntries.add(entry);
286 }
287 } catch (DocTreePathAwareRuntimeException e) {
288 reportError(e.getDocTreePath(), e.getMessage());
289 } catch (IllegalArgumentException e) {
290 reportError(rootPath, e.getMessage());
291 } catch (RuntimeException e) {
292
293 reportError(rootPath, e);
294 }
295 }
296 }
297
298 try {
299 writeProperties(configurationEntries);
300 } catch (IOException e) {
301 reportError("Failed to write properties file: " + e.getMessage());
302 return false;
303 }
304 return true;
305 }
306
307
308
309
310
311
312
313 private void reportError(DocTreePath path, Throwable throwable) {
314 reportError(path, throwable.getMessage());
315 reportError(throwable);
316 }
317
318 private void reportError(Throwable throwable) {
319
320 PrintWriter pw = reporter.getDiagnosticWriter();
321 if (pw == null) {
322 pw = new PrintWriter(System.err);
323 }
324 throwable.printStackTrace(pw);
325 }
326
327
328
329
330
331
332
333 private void reportError(DocTreePath path, String message) {
334 if (path != null) {
335 reporter.print(Diagnostic.Kind.ERROR, path, message);
336 } else {
337 reportError(message);
338 }
339 }
340
341
342
343
344
345
346
347 private void reportError(String message, Throwable throwable) {
348 reporter.print(Diagnostic.Kind.ERROR, message);
349 reportError(throwable);
350 }
351
352
353
354
355
356
357 private void reportError(String message) {
358 reporter.print(Diagnostic.Kind.ERROR, message);
359 }
360
361
362
363
364
365
366
367 private ConfigurationEntry processResolverField(DocTreePath path, VariableElement field) {
368 Objects.requireNonNull(path);
369 Objects.requireNonNull(field);
370 Map<String, UnknownBlockTagTree> blockTags = collectBlockTags(path.getDocComment());
371 if (!blockTags.containsKey("configurationSource")) {
372 return null;
373 }
374 ConfigurationType configurationType = getConfigurationType(path, blockTags);
375 return new ConfigurationEntry(
376 String.valueOf(field.getConstantValue()),
377 getFullBodyContent(path),
378 resolveDefaultValue(path, blockTags).orElse(""),
379 getFullyQualifiedName(field),
380 getSince(path).orElse(""),
381 getConfigurationSource(path, blockTags).orElse(""),
382 configurationType.name(),
383 configurationType.javadocUrl(),
384 isSupportsRepoIdSuffix(path, blockTags),
385 getDeprecated(path, field).orElse(""));
386 }
387
388 private Optional<String> getDeprecated(DocTreePath path, Element element) {
389 Objects.requireNonNull(path, "path must not be null");
390 Objects.requireNonNull(element, "field must not be null");
391
392
393 if (element.getAnnotation(Deprecated.class) == null) {
394
395 return getDeprecated(element.getEnclosingElement());
396 }
397 Optional<? extends DocTree> deprecatedTag = path.getDocComment().getBlockTags().stream()
398 .filter(t -> com.sun.source.doctree.DocTree.Kind.DEPRECATED == t.getKind())
399 .findFirst();
400 if (deprecatedTag.isPresent()) {
401 return Optional.of(renderContent(DocTreePath.getPath(path, deprecatedTag.get()), RenderMode.HTML, true));
402 }
403 return Optional.of("");
404 }
405
406 private Optional<String> getDeprecated(Element element) {
407 if (element == null) {
408 return Optional.empty();
409 }
410 DocCommentTree docCommentTree = docTrees.getDocCommentTree(element);
411 if (docCommentTree == null) {
412 if (element.getAnnotation(Deprecated.class) != null) {
413 return Optional.of("");
414 }
415
416 return getDeprecated(element.getEnclosingElement());
417 } else {
418 return getDeprecated(new DocTreePath(docTrees.getPath(element), docCommentTree), element);
419 }
420 }
421
422 private boolean isSupportsRepoIdSuffix(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
423 UnknownBlockTagTree repoIdTag = blockTags.get("configurationRepoIdSuffix");
424 if (repoIdTag != null) {
425 String content = renderContent(DocTreePath.getPath(path, repoIdTag), RenderMode.PLAIN, true);
426 return "yes".equalsIgnoreCase(content) || "true".equalsIgnoreCase(content);
427 }
428 return false;
429 }
430
431
432
433
434
435
436
437
438
439 private ConfigurationEntry processMavenField(DocTreePath path, VariableElement field) {
440 AnnotationMirror config = getAnnotation(field, MAVEN_CONFIG_ANNOTATION);
441 if (config == null) {
442 return null;
443 }
444
445 String source = "USER_PROPERTIES";
446 String defaultValue = "";
447 String configurationType = "java.lang.String";
448 for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> attribute :
449 config.getElementValues().entrySet()) {
450 String name = attribute.getKey().getSimpleName().toString();
451 Object value = attribute.getValue().getValue();
452 switch (name) {
453 case "source":
454 source = value instanceof VariableElement variableElement
455 ? variableElement.getSimpleName().toString()
456 : String.valueOf(value);
457 break;
458 case "defaultValue":
459 defaultValue = String.valueOf(value);
460 break;
461 case "type":
462 configurationType = String.valueOf(value);
463 break;
464 default:
465 break;
466 }
467 }
468
469 source = source.toLowerCase(Locale.ROOT);
470 switch (source) {
471 case "model":
472 source = "Model properties";
473 break;
474 case "user_properties":
475 source = "User properties";
476 break;
477 case "system_properties":
478 source = "System properties";
479 break;
480 default:
481 break;
482 }
483
484 if (configurationType.startsWith("java.lang.")) {
485 configurationType = configurationType.substring("java.lang.".length());
486 } else if (configurationType.startsWith("java.util.")) {
487 configurationType = configurationType.substring("java.util.".length());
488 }
489 return new ConfigurationEntry(
490 String.valueOf(field.getConstantValue()),
491 path.getDocComment() != null ? getFullBodyContent(path) : "",
492 Objects.toString(defaultValue, ""),
493 getFullyQualifiedName(field),
494 getSince(path).orElse(""),
495 source,
496 configurationType,
497 "",
498 false,
499 getDeprecated(path, field).orElse(""));
500 }
501
502 private AnnotationMirror getAnnotation(Element element, String fqName) {
503 for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
504 Element annotationElement = annotation.getAnnotationType().asElement();
505 if (annotationElement instanceof TypeElement
506 && ((TypeElement) annotationElement).getQualifiedName().contentEquals(fqName)) {
507 return annotation;
508 }
509 }
510 return null;
511 }
512
513 private void writeProperties(List<ConfigurationEntry> configurationEntries) throws IOException {
514 Properties properties = new Properties();
515 properties.setProperty("keys.count", String.valueOf(configurationEntries.size()));
516 for (int i = 0; i < configurationEntries.size(); i++) {
517 ConfigurationEntry entry = configurationEntries.get(i);
518 writeEntry(properties, entry, "keys." + i + ".");
519 }
520 if (output.getParent() != null) {
521 Files.createDirectories(output.getParent());
522 }
523 try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
524 properties.store(writer, "Generated by ConfigurationCollectorDoclet - DO NOT EDIT");
525 }
526 }
527
528 private void writeEntry(Properties properties, ConfigurationEntry entry, String prefix) {
529 properties.setProperty(prefix + "key", entry.key());
530 properties.setProperty(prefix + "defaultValue", entry.defaultValue());
531 properties.setProperty(prefix + "fqName", entry.fqName());
532 properties.setProperty(prefix + "description", entry.description());
533 properties.setProperty(prefix + "since", entry.since());
534 properties.setProperty(prefix + "configurationSource", entry.source());
535 properties.setProperty(prefix + "configurationType", entry.type());
536 properties.setProperty(prefix + "configurationTypeJavadocUrl", entry.typeJavadocUrl());
537 properties.setProperty(prefix + "supportRepoIdSuffix", toYesNo(entry.supportsRepoIdSuffix()));
538 properties.setProperty(prefix + "deprecated", entry.deprecated());
539 }
540
541
542
543 private Map<String, UnknownBlockTagTree> collectBlockTags(DocCommentTree docComment) {
544 Map<String, UnknownBlockTagTree> result = new LinkedHashMap<>();
545 if (docComment == null) {
546 return result;
547 }
548 for (DocTree tag : docComment.getBlockTags()) {
549 if (tag instanceof UnknownBlockTagTree unknownBlockTree) {
550 result.put(unknownBlockTree.getTagName(), unknownBlockTree);
551 }
552 }
553 return result;
554 }
555
556 private String getFullBodyContent(DocTreePath path) {
557 return renderContent(path, RenderMode.HTML, true, path.getDocComment().getFullBody());
558 }
559
560 private Optional<String> resolveDefaultValue(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
561 UnknownBlockTagTree defaultValueTag = blockTags.get("configurationDefaultValue");
562 if (defaultValueTag == null) {
563 return Optional.empty();
564 }
565 DocTreePath defaultValuePath = DocTreePath.getPath(path, defaultValueTag);
566 for (DocTree tree : defaultValueTag.getContent()) {
567 if (tree instanceof LinkTree link) {
568 String signature = link.getReference().getSignature();
569 DocTreePath linkTreePath = DocTreePath.getPath(path, tree);
570
571
572 VariableElement referenced = resolveReferencedField(linkTreePath, link);
573 String value = referenced != null ? lookupConstant(referenced) : null;
574 if (value == null) {
575
576
577 DocTreePath linkRefPath = DocTreePath.getPath(linkTreePath, link.getReference());
578 throw new DocTreePathAwareRuntimeException(
579 linkRefPath != null ? linkRefPath : linkTreePath,
580 "Could not resolve link to determine default value: " + signature);
581 }
582 return Optional.ofNullable(value);
583 }
584 }
585
586
587 return Optional.of(renderContent(defaultValuePath, RenderMode.PLAIN, true));
588 }
589
590
591
592
593
594
595 private VariableElement resolveReferencedField(DocTreePath path, LinkTree link) {
596 DocTreePath refPath = DocTreePath.getPath(path, link.getReference());
597 if (refPath == null) {
598 return null;
599 }
600 Element element = docTrees.getElement(refPath);
601 return element instanceof VariableElement variableElement ? variableElement : null;
602 }
603
604 private String lookupConstant(VariableElement field) {
605 if (field.getConstantValue() != null) {
606 Object value = field.getConstantValue();
607 if (value instanceof String) {
608 return "\"" + value + "\"";
609 } else {
610 return String.valueOf(field.getConstantValue());
611 }
612 }
613
614 if (field.getKind() == ElementKind.ENUM_CONSTANT) {
615 return field.getSimpleName().toString();
616 }
617
618
619 return resolveEnumReference(field);
620 }
621
622
623
624
625
626
627 private String resolveEnumReference(VariableElement field) {
628 if (!(docTrees.getTree(field) instanceof VariableTree variableTree)) {
629 return null;
630 }
631 ExpressionTree initializer = variableTree.getInitializer();
632 String enumConstant = null;
633 if (initializer instanceof MemberSelectTree memberSelectTree) {
634
635 enumConstant = memberSelectTree.getIdentifier().toString();
636 } else if (initializer instanceof IdentifierTree identifierTree) {
637
638 enumConstant = identifierTree.getName().toString();
639 }
640 if (enumConstant == null) {
641 return null;
642 }
643 return enumConstant;
644 }
645
646 private Optional<LinkTree> getFirstLinkInBlockTag(UnknownBlockTagTree tag) {
647 for (DocTree tree : tag.getContent()) {
648 if (tree instanceof LinkTree link) {
649 return Optional.of(link);
650 }
651 }
652 return Optional.empty();
653 }
654
655
656
657
658
659
660
661 private String getType(DocTreePath path, LinkTree link) {
662 String signature = link.getReference().getSignature();
663 if (signature.contains("#")) {
664
665 DocTreePath linkRefPath = DocTreePath.getPath(path, link.getReference());
666 throw new DocTreePathAwareRuntimeException(
667 linkRefPath != null ? linkRefPath : path,
668 "Expected a class link, but got a member reference: " + signature);
669 }
670
671
672 return resolveReferencedType(path, link.getReference())
673 .map(t -> t.getQualifiedName().toString())
674 .orElse(signature);
675 }
676
677
678
679
680
681
682 private Optional<TypeElement> resolveReferencedType(DocTreePath path, ReferenceTree reference) {
683
684 DocTreePath refPath = DocTreePath.getPath(path, reference);
685 if (refPath == null) {
686 return Optional.empty();
687 }
688 Element element = docTrees.getElement(refPath);
689 return element instanceof TypeElement typeElement ? Optional.of(typeElement) : Optional.empty();
690 }
691
692 enum RenderMode {
693
694 PLAIN,
695
696 HTML
697 }
698
699 private String renderContent(DocTreePath docTreePath, RenderMode mode, boolean trim) {
700 return renderContent(docTreePath, mode, trim, null);
701 }
702
703
704
705
706
707
708
709
710
711
712
713
714 private String renderContent(
715 DocTreePath docTreePath, RenderMode mode, boolean trim, Collection<? extends DocTree> docTreesToRender) {
716 Objects.requireNonNull(docTreePath, "docTreePath must not be null");
717 StringBuilder sb = new StringBuilder();
718 SimpleDocTreeVisitor<String, Void> visitor = new SimpleDocTreeVisitor<String, Void>() {
719 @Override
720 public String visitText(TextTree node, Void p) {
721 return escape(mode, node.getBody());
722 }
723
724 @Override
725 public String visitLink(LinkTree node, Void p) {
726 String ref = node.getReference() != null ? node.getReference().getSignature() : "";
727 List<? extends DocTree> labelTrees = node.getLabel();
728 String label = (labelTrees != null && !labelTrees.isEmpty())
729 ? renderContent(docTreePath, mode, false, labelTrees)
730 : "";
731 String text = label.isEmpty() ? ref : label;
732 String rendered = node.getKind() == DocTree.Kind.LINK_PLAIN ? escape(mode, text) : renderAsCode(text);
733 if (mode == RenderMode.HTML) {
734 VariableElement referenced = resolveReferencedField(docTreePath, node);
735 String configurationKey = getConfigurationKey(referenced);
736 if (configurationKey != null) {
737 rendered = node.getKind() == DocTree.Kind.LINK_PLAIN
738 ? escape(mode, configurationKey)
739 : renderAsCode(configurationKey);
740 return "<a href=\"#" + escape(mode, configurationKey) + "\">" + rendered + "</a>";
741 }
742 Optional<String> javadocUrl = getJavadocUrl(docTreePath, node.getReference());
743 if (javadocUrl.isPresent()) {
744 return "<a href=\"" + javadocUrl.get() + "\">" + rendered + "</a>";
745 }
746 }
747 return rendered;
748 }
749
750 @Override
751 public String visitLiteral(LiteralTree node, Void p) {
752 if (node.getKind() == DocTree.Kind.CODE) {
753 return renderAsCode(node.getBody().getBody());
754 } else {
755 return escape(mode, node.getBody().getBody());
756 }
757 }
758
759 @Override
760 public String visitSystemProperty(SystemPropertyTree node, Void p) {
761 return renderAsCode(node.getPropertyName().toString());
762 }
763
764 private String renderAsCode(String text) {
765 if (mode == RenderMode.HTML) {
766 return "<code>" + escape(mode, text) + "</code>";
767 } else {
768 return escape(mode, text);
769 }
770 }
771
772 @Override
773 public String visitValue(ValueTree node, Void p) {
774 if (node.getReference() != null) {
775 DocTreePath refPath = DocTreePath.getPath(docTreePath, node.getReference());
776 if (refPath != null) {
777 Element element = docTrees.getElement(refPath);
778 if (element instanceof VariableElement ve) {
779 String value = lookupConstant(ve);
780 if (value != null) {
781 return renderAsCode(value);
782 }
783 }
784 }
785 }
786
787 String ref = node.getReference() != null ? node.getReference().getSignature() : "";
788 return renderAsCode(ref);
789 }
790
791 @Override
792 public String visitEntity(EntityTree node, Void p) {
793 return "&" + node.getName() + ";";
794 }
795
796 @Override
797 public String visitUnknownBlockTag(UnknownBlockTagTree node, Void p) {
798 StringBuilder sb = new StringBuilder();
799 node.getContent().forEach(child -> sb.append(child.accept(this, p)));
800 return sb.toString();
801 }
802
803 @Override
804 public String visitSince(SinceTree node, Void p) {
805 return escape(mode, node.getBody().toString());
806 }
807
808 @Override
809 public String visitDeprecated(DeprecatedTree node, Void p) {
810 StringBuilder sb = new StringBuilder();
811 node.getBody().forEach(child -> sb.append(child.accept(this, p)));
812 return sb.toString();
813 }
814
815 @Override
816 protected String defaultAction(DocTree node, Void p) {
817
818
819
820 return node.toString();
821 }
822 };
823 if (docTreesToRender == null) {
824 docTreesToRender = Collections.singleton(docTreePath.getLeaf());
825 }
826 for (DocTree docTreeToRender : docTreesToRender) {
827 sb.append(docTreeToRender.accept(visitor, null));
828 }
829
830 if (trim) {
831
832
833
834 return sb.toString().trim().replaceAll("\\s+", " ");
835 } else {
836 return sb.toString();
837 }
838 }
839
840 private static String escape(RenderMode mode, String text) {
841 if (mode == RenderMode.HTML) {
842 return text.replace("&", "&").replace("<", "<").replace(">", ">");
843 } else {
844 return text;
845 }
846 }
847
848 private Optional<String> getSince(DocTreePath path) {
849 String since = getSinceTag(path);
850 if (since == null && path.getTreePath().getParentPath() != null) {
851
852 return getSince(docTrees.getElement(path.getTreePath().getParentPath()));
853 }
854 return Optional.ofNullable(since);
855 }
856
857 private Optional<String> getSince(Element element) {
858 if (element == null) {
859 return Optional.empty();
860 }
861 DocCommentTree docComment = docTrees.getDocCommentTree(element);
862 if (docComment != null) {
863 DocTreePath path = new DocTreePath(docTrees.getPath(element), docComment);
864 Optional<String> since = getSince(path);
865 if (since.isPresent()) {
866 return since;
867 }
868 }
869
870 return getSince(element.getEnclosingElement());
871 }
872
873 private String getSinceTag(DocTreePath path) {
874 if (path == null) {
875
876 return null;
877 }
878 for (DocTree tag : path.getDocComment().getBlockTags()) {
879 if (tag instanceof SinceTree) {
880 return renderContent(DocTreePath.getPath(path, tag), RenderMode.PLAIN, true);
881 }
882 }
883 return null;
884 }
885
886 private ConfigurationType getConfigurationType(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
887 UnknownBlockTagTree typeTag = blockTags.get("configurationType");
888 if (typeTag == null) {
889 throw new IllegalStateException("Missing block tag @configurationType");
890 }
891 DocTreePath configurationTypePath = DocTreePath.getPath(path, typeTag);
892 LinkTree linkTree = getFirstLinkInBlockTag(typeTag)
893 .orElseThrow(() -> new DocTreePathAwareRuntimeException(
894 configurationTypePath, "No valid {@link ...} reference found in @" + typeTag.getTagName()));
895
896 String type = getType(configurationTypePath, linkTree);
897 String javadocUrl =
898 getJavadocUrl(configurationTypePath, linkTree.getReference()).orElse("");
899 String javaLangPackage = "java.lang.";
900 if (type.startsWith(javaLangPackage)) {
901 type = type.substring(javaLangPackage.length());
902 }
903 return new ConfigurationType(type, javadocUrl);
904 }
905
906 private String getConfigurationKey(VariableElement field) {
907 if (field == null || !(field.getConstantValue() instanceof String key)) {
908 return null;
909 }
910 if (mode == Mode.MAVEN) {
911 return getAnnotation(field, MAVEN_CONFIG_ANNOTATION) != null ? key : null;
912 }
913 DocCommentTree docComment = docTrees.getDocCommentTree(field);
914 return docComment != null && collectBlockTags(docComment).containsKey("configurationSource") ? key : null;
915 }
916
917 private Optional<String> getJavadocUrl(DocTreePath path, ReferenceTree reference) {
918 if (reference == null) {
919 return Optional.empty();
920 }
921 DocTreePath referencePath = DocTreePath.getPath(path, reference);
922 if (referencePath == null) {
923 return Optional.empty();
924 }
925 Element element = docTrees.getElement(referencePath);
926 if (element == null) {
927 return Optional.empty();
928 }
929 return createJavadocReference(element).flatMap(javadocReference -> {
930 try {
931 return Optional.of(
932 javadocLinkGenerator.createLink(javadocReference).toString());
933 } catch (IllegalArgumentException e) {
934 return Optional.empty();
935 }
936 });
937 }
938
939 private Optional<FullyQualifiedJavadocReference> createJavadocReference(Element element) {
940 boolean external = !isInternalJavadocElement(element);
941 if (element instanceof ModuleElement moduleElement) {
942 return Optional.of(new FullyQualifiedJavadocReference(
943 Optional.of(moduleElement.getQualifiedName().toString()),
944 Optional.empty(),
945 Optional.empty(),
946 Optional.empty(),
947 Optional.empty(),
948 Optional.empty(),
949 external));
950 }
951
952 Optional<String> moduleName = external ? Optional.empty() : getModuleName(element);
953 if (element instanceof PackageElement packageElement) {
954 return Optional.of(new FullyQualifiedJavadocReference(
955 moduleName,
956 Optional.of(packageElement.getQualifiedName().toString()),
957 Optional.empty(),
958 Optional.empty(),
959 Optional.empty(),
960 Optional.empty(),
961 external));
962 }
963
964 TypeElement declaringType;
965 Optional<String> member = Optional.empty();
966 Optional<MemberType> memberType = Optional.empty();
967 if (element instanceof TypeElement typeElement) {
968 declaringType = typeElement;
969 } else if (element instanceof VariableElement variableElement
970 && variableElement.getEnclosingElement() instanceof TypeElement typeElement) {
971 declaringType = typeElement;
972 member = Optional.of(variableElement.getSimpleName().toString());
973 memberType = Optional.of(MemberType.FIELD);
974 } else if (element instanceof ExecutableElement executableElement
975 && executableElement.getEnclosingElement() instanceof TypeElement typeElement) {
976 declaringType = typeElement;
977 String executableName = executableElement.getKind() == ElementKind.CONSTRUCTOR
978 ? typeElement.getSimpleName().toString()
979 : executableElement.getSimpleName().toString();
980 String parameterTypes = String.join(
981 ",",
982 executableElement.getParameters().stream()
983 .map(parameter -> getFullyQualifiedName(types.erasure(parameter.asType())))
984 .toList());
985 member = Optional.of(executableName + "(" + parameterTypes + ")");
986 memberType = Optional.of(
987 executableElement.getKind() == ElementKind.CONSTRUCTOR
988 ? MemberType.CONSTRUCTOR
989 : MemberType.METHOD);
990 } else {
991 return Optional.empty();
992 }
993
994 PackageElement packageElement = elements.getPackageOf(declaringType);
995 String packageName = packageElement.getQualifiedName().toString();
996 String qualifiedName = declaringType.getQualifiedName().toString();
997 String className = packageName.isEmpty() ? qualifiedName : qualifiedName.substring(packageName.length() + 1);
998 return Optional.of(new FullyQualifiedJavadocReference(
999 moduleName,
1000 Optional.of(packageName),
1001 Optional.of(className),
1002 member,
1003 memberType,
1004 Optional.empty(),
1005 external));
1006 }
1007
1008 private Optional<String> getModuleName(Element element) {
1009 ModuleElement module = elements.getModuleOf(element);
1010 return module != null && !module.isUnnamed()
1011 ? Optional.of(module.getQualifiedName().toString())
1012 : Optional.empty();
1013 }
1014
1015 private boolean isInternalJavadocElement(Element element) {
1016 if (element instanceof ModuleElement) {
1017 return docTrees.getPath(element) != null;
1018 }
1019 if (element instanceof PackageElement packageElement) {
1020 return docTrees.getPath(element) != null || isPackageInInternalSourceTree(packageElement);
1021 }
1022
1023 TypeElement topLevelType = null;
1024 for (Element current = element;
1025 current != null && !(current instanceof PackageElement) && !(current instanceof ModuleElement);
1026 current = current.getEnclosingElement()) {
1027 if (current instanceof TypeElement typeElement) {
1028 topLevelType = typeElement;
1029 }
1030 if (isJavadocDeclaration(current)
1031 && !current.getModifiers().contains(Modifier.PUBLIC)
1032 && !current.getModifiers().contains(Modifier.PROTECTED)) {
1033 return false;
1034 }
1035 }
1036 return topLevelType != null
1037 && (docTrees.getPath(topLevelType) != null || isTypeInInternalSourceTree(topLevelType));
1038 }
1039
1040 private boolean isPackageInInternalSourceTree(PackageElement packageElement) {
1041 Path packagePath = getPackagePath(packageElement);
1042 return internalJavadocSourceRoots.stream()
1043 .map(sourceRoot -> sourceRoot.resolve(packagePath))
1044 .anyMatch(Files::isDirectory);
1045 }
1046
1047 private boolean isTypeInInternalSourceTree(TypeElement topLevelType) {
1048 Path packagePath = getPackagePath(elements.getPackageOf(topLevelType));
1049 Path sourceFile = packagePath.resolve(topLevelType.getSimpleName() + ".java");
1050 return internalJavadocSourceRoots.stream()
1051 .map(sourceRoot -> sourceRoot.resolve(sourceFile))
1052 .anyMatch(Files::isRegularFile);
1053 }
1054
1055 private static Path getPackagePath(PackageElement packageElement) {
1056 String packageName = packageElement.getQualifiedName().toString();
1057 return packageName.isEmpty() ? Path.of("") : Path.of(packageName.replace('.', '/'));
1058 }
1059
1060 private static boolean isJavadocDeclaration(Element element) {
1061 ElementKind kind = element.getKind();
1062 return kind.isClass()
1063 || kind.isInterface()
1064 || kind == ElementKind.FIELD
1065 || kind == ElementKind.ENUM_CONSTANT
1066 || kind == ElementKind.METHOD
1067 || kind == ElementKind.CONSTRUCTOR;
1068 }
1069
1070 private Optional<String> getConfigurationSource(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
1071 UnknownBlockTagTree configurationSourceTag = blockTags.get("configurationSource");
1072 if (configurationSourceTag == null) {
1073 return Optional.empty();
1074 }
1075 DocTreePath configurationSourcePath = DocTreePath.getPath(path, configurationSourceTag);
1076 LinkTree linkTree = getFirstLinkInBlockTag(configurationSourceTag)
1077 .orElseThrow(() -> new DocTreePathAwareRuntimeException(
1078 configurationSourcePath,
1079 "No valid {@link ...} reference found in @" + configurationSourceTag.getTagName()));
1080
1081
1082
1083 MethodReference methodReference = getReferencedMethod(configurationSourcePath, linkTree);
1084 if (methodReference.equals(METHOD_REFERENCE_SESSION_CONFIGURATION)) {
1085 return Optional.of("Session Configuration");
1086 } else if (methodReference.equals(METHOD_REFERENCE_SYSTEM_PROPERTY)) {
1087 return Optional.of("Java System Properties");
1088 } else {
1089 reporter.print(
1090 Diagnostic.Kind.WARNING,
1091 path,
1092 "Unknown configuration source: " + linkTree.getReference().getSignature()
1093 + ", using raw signature as source");
1094 return Optional.of(linkTree.getReference().getSignature());
1095 }
1096 }
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107 protected record MethodReference(
1108 String fullyQualifiedClassName, String methodName, List<String> fullyQualifiedParameterTypes) {}
1109
1110 private MethodReference getReferencedMethod(DocTreePath path, LinkTree link) {
1111 ExecutableElement ee = getReferencedExecutableElement(path, link);
1112 String fullyQualifiedClassName =
1113 ((TypeElement) ee.getEnclosingElement()).getQualifiedName().toString();
1114 String methodName = ee.getSimpleName().toString();
1115 List<String> parameterTypes = ee.getParameters().stream()
1116 .map(p -> getFullyQualifiedName(p.asType()))
1117 .toList();
1118 return new MethodReference(fullyQualifiedClassName, methodName, parameterTypes);
1119 }
1120
1121 static String getFullyQualifiedName(Element e) {
1122 return new SimpleElementVisitor14<String, Void>() {
1123 @Override
1124 public String visitModule(ModuleElement e, Void p) {
1125 return e.getQualifiedName().toString();
1126 }
1127
1128 @Override
1129 public String visitPackage(PackageElement e, Void p) {
1130 return e.getQualifiedName().toString();
1131 }
1132
1133 @Override
1134 public String visitType(TypeElement e, Void p) {
1135 return e.getQualifiedName().toString();
1136 }
1137
1138 @Override
1139 protected String defaultAction(Element e, Void p) {
1140 return visit(e.getEnclosingElement()) + "." + e.getSimpleName();
1141 }
1142 }.visit(e);
1143 }
1144
1145 static String getFullyQualifiedName(TypeMirror e) {
1146 return new SimpleTypeVisitor14<String, Void>() {
1147 @Override
1148 public String visitDeclared(DeclaredType t, Void p) {
1149 Element e = t.asElement();
1150 if (e instanceof TypeElement typeElement) {
1151 return typeElement.getQualifiedName().toString();
1152 }
1153 return super.visitDeclared(t, p);
1154 }
1155
1156 @Override
1157 public String visitPrimitive(PrimitiveType t, Void p) {
1158 return t.toString();
1159 }
1160
1161 @Override
1162 protected String defaultAction(TypeMirror e, Void p) {
1163 return e.toString();
1164 }
1165 }.visit(e);
1166 }
1167
1168 private ExecutableElement getReferencedExecutableElement(DocTreePath path, LinkTree link) {
1169 DocTreePath linkRefPath = DocTreePath.getPath(path, link.getReference());
1170 if (linkRefPath == null) {
1171 throw new DocTreePathAwareRuntimeException(
1172 path,
1173 "Could not resolve link reference: " + link.getReference().getSignature());
1174 }
1175 Element element = docTrees.getElement(linkRefPath);
1176 if (element instanceof ExecutableElement ee) {
1177 return ee;
1178 } else {
1179 throw new DocTreePathAwareRuntimeException(
1180 linkRefPath, "Expected an executable element, but got: " + element);
1181 }
1182 }
1183
1184 private static String toYesNo(boolean value) {
1185 return value ? "Yes" : "No";
1186 }
1187
1188 private static URI parseJavadocUrl(String value) {
1189 URI uri = URI.create(value);
1190 if (uri.getQuery() != null || uri.getFragment() != null) {
1191 throw new IllegalArgumentException("Javadoc base URL must not contain a query or fragment: " + value);
1192 }
1193 return value.endsWith("/") ? uri : URI.create(value + "/");
1194 }
1195
1196
1197
1198
1199 private static final class SingleArgumentOption implements Option {
1200 private final List<String> names;
1201 private final String description;
1202 private final String parameters;
1203 private final java.util.function.Consumer<String> processor;
1204
1205 SingleArgumentOption(
1206 List<String> names,
1207 String description,
1208 String parameters,
1209 java.util.function.Consumer<String> processor) {
1210 this.names = names;
1211 this.description = description;
1212 this.parameters = parameters;
1213 this.processor = processor;
1214 }
1215
1216 @Override
1217 public int getArgumentCount() {
1218 return 1;
1219 }
1220
1221 @Override
1222 public String getDescription() {
1223 return description;
1224 }
1225
1226 @Override
1227 public Kind getKind() {
1228 return Kind.STANDARD;
1229 }
1230
1231 @Override
1232 public List<String> getNames() {
1233 return names;
1234 }
1235
1236 @Override
1237 public String getParameters() {
1238 return parameters;
1239 }
1240
1241 @Override
1242 public boolean process(String option, List<String> arguments) {
1243 processor.accept(arguments.get(0));
1244
1245
1246 return true;
1247 }
1248 }
1249 }