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 return "<a href=\"#" + escape(mode, configurationKey) + "\">" + rendered + "</a>";
738 }
739 Optional<String> javadocUrl = getJavadocUrl(docTreePath, node.getReference());
740 if (javadocUrl.isPresent()) {
741 return "<a href=\"" + javadocUrl.get() + "\">" + rendered + "</a>";
742 }
743 }
744 return rendered;
745 }
746
747 @Override
748 public String visitLiteral(LiteralTree node, Void p) {
749 if (node.getKind() == DocTree.Kind.CODE) {
750 return renderAsCode(node.getBody().getBody());
751 } else {
752 return escape(mode, node.getBody().getBody());
753 }
754 }
755
756 @Override
757 public String visitSystemProperty(SystemPropertyTree node, Void p) {
758 return renderAsCode(node.getPropertyName().toString());
759 }
760
761 private String renderAsCode(String text) {
762 if (mode == RenderMode.HTML) {
763 return "<code>" + escape(mode, text) + "</code>";
764 } else {
765 return escape(mode, text);
766 }
767 }
768
769 @Override
770 public String visitValue(ValueTree node, Void p) {
771 if (node.getReference() != null) {
772 DocTreePath refPath = DocTreePath.getPath(docTreePath, node.getReference());
773 if (refPath != null) {
774 Element element = docTrees.getElement(refPath);
775 if (element instanceof VariableElement ve) {
776 String value = lookupConstant(ve);
777 if (value != null) {
778 return renderAsCode(value);
779 }
780 }
781 }
782 }
783
784 String ref = node.getReference() != null ? node.getReference().getSignature() : "";
785 return renderAsCode(ref);
786 }
787
788 @Override
789 public String visitEntity(EntityTree node, Void p) {
790 return "&" + node.getName() + ";";
791 }
792
793 @Override
794 public String visitUnknownBlockTag(UnknownBlockTagTree node, Void p) {
795 StringBuilder sb = new StringBuilder();
796 node.getContent().forEach(child -> sb.append(child.accept(this, p)));
797 return sb.toString();
798 }
799
800 @Override
801 public String visitSince(SinceTree node, Void p) {
802 return escape(mode, node.getBody().toString());
803 }
804
805 @Override
806 public String visitDeprecated(DeprecatedTree node, Void p) {
807 StringBuilder sb = new StringBuilder();
808 node.getBody().forEach(child -> sb.append(child.accept(this, p)));
809 return sb.toString();
810 }
811
812 @Override
813 protected String defaultAction(DocTree node, Void p) {
814
815
816
817 return node.toString();
818 }
819 };
820 if (docTreesToRender == null) {
821 docTreesToRender = Collections.singleton(docTreePath.getLeaf());
822 }
823 for (DocTree docTreeToRender : docTreesToRender) {
824 sb.append(docTreeToRender.accept(visitor, null));
825 }
826
827 if (trim) {
828
829
830
831 return sb.toString().trim().replaceAll("\\s+", " ");
832 } else {
833 return sb.toString();
834 }
835 }
836
837 private static String escape(RenderMode mode, String text) {
838 if (mode == RenderMode.HTML) {
839 return text.replace("&", "&").replace("<", "<").replace(">", ">");
840 } else {
841 return text;
842 }
843 }
844
845 private Optional<String> getSince(DocTreePath path) {
846 String since = getSinceTag(path);
847 if (since == null && path.getTreePath().getParentPath() != null) {
848
849 return getSince(docTrees.getElement(path.getTreePath().getParentPath()));
850 }
851 return Optional.ofNullable(since);
852 }
853
854 private Optional<String> getSince(Element element) {
855 if (element == null) {
856 return Optional.empty();
857 }
858 DocCommentTree docComment = docTrees.getDocCommentTree(element);
859 if (docComment != null) {
860 DocTreePath path = new DocTreePath(docTrees.getPath(element), docComment);
861 Optional<String> since = getSince(path);
862 if (since.isPresent()) {
863 return since;
864 }
865 }
866
867 return getSince(element.getEnclosingElement());
868 }
869
870 private String getSinceTag(DocTreePath path) {
871 if (path == null) {
872
873 return null;
874 }
875 for (DocTree tag : path.getDocComment().getBlockTags()) {
876 if (tag instanceof SinceTree) {
877 return renderContent(DocTreePath.getPath(path, tag), RenderMode.PLAIN, true);
878 }
879 }
880 return null;
881 }
882
883 private ConfigurationType getConfigurationType(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
884 UnknownBlockTagTree typeTag = blockTags.get("configurationType");
885 if (typeTag == null) {
886 throw new IllegalStateException("Missing block tag @configurationType");
887 }
888 DocTreePath configurationTypePath = DocTreePath.getPath(path, typeTag);
889 LinkTree linkTree = getFirstLinkInBlockTag(typeTag)
890 .orElseThrow(() -> new DocTreePathAwareRuntimeException(
891 configurationTypePath, "No valid {@link ...} reference found in @" + typeTag.getTagName()));
892
893 String type = getType(configurationTypePath, linkTree);
894 String javadocUrl =
895 getJavadocUrl(configurationTypePath, linkTree.getReference()).orElse("");
896 String javaLangPackage = "java.lang.";
897 if (type.startsWith(javaLangPackage)) {
898 type = type.substring(javaLangPackage.length());
899 }
900 return new ConfigurationType(type, javadocUrl);
901 }
902
903 private String getConfigurationKey(VariableElement field) {
904 if (field == null || !(field.getConstantValue() instanceof String key)) {
905 return null;
906 }
907 if (mode == Mode.MAVEN) {
908 return getAnnotation(field, MAVEN_CONFIG_ANNOTATION) != null ? key : null;
909 }
910 DocCommentTree docComment = docTrees.getDocCommentTree(field);
911 return docComment != null && collectBlockTags(docComment).containsKey("configurationSource") ? key : null;
912 }
913
914 private Optional<String> getJavadocUrl(DocTreePath path, ReferenceTree reference) {
915 if (reference == null) {
916 return Optional.empty();
917 }
918 DocTreePath referencePath = DocTreePath.getPath(path, reference);
919 if (referencePath == null) {
920 return Optional.empty();
921 }
922 Element element = docTrees.getElement(referencePath);
923 if (element == null) {
924 return Optional.empty();
925 }
926 return createJavadocReference(element).flatMap(javadocReference -> {
927 try {
928 return Optional.of(
929 javadocLinkGenerator.createLink(javadocReference).toString());
930 } catch (IllegalArgumentException e) {
931 return Optional.empty();
932 }
933 });
934 }
935
936 private Optional<FullyQualifiedJavadocReference> createJavadocReference(Element element) {
937 boolean external = !isInternalJavadocElement(element);
938 if (element instanceof ModuleElement moduleElement) {
939 return Optional.of(new FullyQualifiedJavadocReference(
940 Optional.of(moduleElement.getQualifiedName().toString()),
941 Optional.empty(),
942 Optional.empty(),
943 Optional.empty(),
944 Optional.empty(),
945 Optional.empty(),
946 external));
947 }
948
949 Optional<String> moduleName = external ? Optional.empty() : getModuleName(element);
950 if (element instanceof PackageElement packageElement) {
951 return Optional.of(new FullyQualifiedJavadocReference(
952 moduleName,
953 Optional.of(packageElement.getQualifiedName().toString()),
954 Optional.empty(),
955 Optional.empty(),
956 Optional.empty(),
957 Optional.empty(),
958 external));
959 }
960
961 TypeElement declaringType;
962 Optional<String> member = Optional.empty();
963 Optional<MemberType> memberType = Optional.empty();
964 if (element instanceof TypeElement typeElement) {
965 declaringType = typeElement;
966 } else if (element instanceof VariableElement variableElement
967 && variableElement.getEnclosingElement() instanceof TypeElement typeElement) {
968 declaringType = typeElement;
969 member = Optional.of(variableElement.getSimpleName().toString());
970 memberType = Optional.of(MemberType.FIELD);
971 } else if (element instanceof ExecutableElement executableElement
972 && executableElement.getEnclosingElement() instanceof TypeElement typeElement) {
973 declaringType = typeElement;
974 String executableName = executableElement.getKind() == ElementKind.CONSTRUCTOR
975 ? typeElement.getSimpleName().toString()
976 : executableElement.getSimpleName().toString();
977 String parameterTypes = String.join(
978 ",",
979 executableElement.getParameters().stream()
980 .map(parameter -> getFullyQualifiedName(types.erasure(parameter.asType())))
981 .toList());
982 member = Optional.of(executableName + "(" + parameterTypes + ")");
983 memberType = Optional.of(
984 executableElement.getKind() == ElementKind.CONSTRUCTOR
985 ? MemberType.CONSTRUCTOR
986 : MemberType.METHOD);
987 } else {
988 return Optional.empty();
989 }
990
991 PackageElement packageElement = elements.getPackageOf(declaringType);
992 String packageName = packageElement.getQualifiedName().toString();
993 String qualifiedName = declaringType.getQualifiedName().toString();
994 String className = packageName.isEmpty() ? qualifiedName : qualifiedName.substring(packageName.length() + 1);
995 return Optional.of(new FullyQualifiedJavadocReference(
996 moduleName,
997 Optional.of(packageName),
998 Optional.of(className),
999 member,
1000 memberType,
1001 Optional.empty(),
1002 external));
1003 }
1004
1005 private Optional<String> getModuleName(Element element) {
1006 ModuleElement module = elements.getModuleOf(element);
1007 return module != null && !module.isUnnamed()
1008 ? Optional.of(module.getQualifiedName().toString())
1009 : Optional.empty();
1010 }
1011
1012 private boolean isInternalJavadocElement(Element element) {
1013 if (element instanceof ModuleElement) {
1014 return docTrees.getPath(element) != null;
1015 }
1016 if (element instanceof PackageElement packageElement) {
1017 return docTrees.getPath(element) != null || isPackageInInternalSourceTree(packageElement);
1018 }
1019
1020 TypeElement topLevelType = null;
1021 for (Element current = element;
1022 current != null && !(current instanceof PackageElement) && !(current instanceof ModuleElement);
1023 current = current.getEnclosingElement()) {
1024 if (current instanceof TypeElement typeElement) {
1025 topLevelType = typeElement;
1026 }
1027 if (isJavadocDeclaration(current)
1028 && !current.getModifiers().contains(Modifier.PUBLIC)
1029 && !current.getModifiers().contains(Modifier.PROTECTED)) {
1030 return false;
1031 }
1032 }
1033 return topLevelType != null
1034 && (docTrees.getPath(topLevelType) != null || isTypeInInternalSourceTree(topLevelType));
1035 }
1036
1037 private boolean isPackageInInternalSourceTree(PackageElement packageElement) {
1038 Path packagePath = getPackagePath(packageElement);
1039 return internalJavadocSourceRoots.stream()
1040 .map(sourceRoot -> sourceRoot.resolve(packagePath))
1041 .anyMatch(Files::isDirectory);
1042 }
1043
1044 private boolean isTypeInInternalSourceTree(TypeElement topLevelType) {
1045 Path packagePath = getPackagePath(elements.getPackageOf(topLevelType));
1046 Path sourceFile = packagePath.resolve(topLevelType.getSimpleName() + ".java");
1047 return internalJavadocSourceRoots.stream()
1048 .map(sourceRoot -> sourceRoot.resolve(sourceFile))
1049 .anyMatch(Files::isRegularFile);
1050 }
1051
1052 private static Path getPackagePath(PackageElement packageElement) {
1053 String packageName = packageElement.getQualifiedName().toString();
1054 return packageName.isEmpty() ? Path.of("") : Path.of(packageName.replace('.', '/'));
1055 }
1056
1057 private static boolean isJavadocDeclaration(Element element) {
1058 ElementKind kind = element.getKind();
1059 return kind.isClass()
1060 || kind.isInterface()
1061 || kind == ElementKind.FIELD
1062 || kind == ElementKind.ENUM_CONSTANT
1063 || kind == ElementKind.METHOD
1064 || kind == ElementKind.CONSTRUCTOR;
1065 }
1066
1067 private Optional<String> getConfigurationSource(DocTreePath path, Map<String, UnknownBlockTagTree> blockTags) {
1068 UnknownBlockTagTree configurationSourceTag = blockTags.get("configurationSource");
1069 if (configurationSourceTag == null) {
1070 return Optional.empty();
1071 }
1072 DocTreePath configurationSourcePath = DocTreePath.getPath(path, configurationSourceTag);
1073 LinkTree linkTree = getFirstLinkInBlockTag(configurationSourceTag)
1074 .orElseThrow(() -> new DocTreePathAwareRuntimeException(
1075 configurationSourcePath,
1076 "No valid {@link ...} reference found in @" + configurationSourceTag.getTagName()));
1077
1078
1079
1080 MethodReference methodReference = getReferencedMethod(configurationSourcePath, linkTree);
1081 if (methodReference.equals(METHOD_REFERENCE_SESSION_CONFIGURATION)) {
1082 return Optional.of("Session Configuration");
1083 } else if (methodReference.equals(METHOD_REFERENCE_SYSTEM_PROPERTY)) {
1084 return Optional.of("Java System Properties");
1085 } else {
1086 reporter.print(
1087 Diagnostic.Kind.WARNING,
1088 path,
1089 "Unknown configuration source: " + linkTree.getReference().getSignature()
1090 + ", using raw signature as source");
1091 return Optional.of(linkTree.getReference().getSignature());
1092 }
1093 }
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104 protected record MethodReference(
1105 String fullyQualifiedClassName, String methodName, List<String> fullyQualifiedParameterTypes) {}
1106
1107 private MethodReference getReferencedMethod(DocTreePath path, LinkTree link) {
1108 ExecutableElement ee = getReferencedExecutableElement(path, link);
1109 String fullyQualifiedClassName =
1110 ((TypeElement) ee.getEnclosingElement()).getQualifiedName().toString();
1111 String methodName = ee.getSimpleName().toString();
1112 List<String> parameterTypes = ee.getParameters().stream()
1113 .map(p -> getFullyQualifiedName(p.asType()))
1114 .toList();
1115 return new MethodReference(fullyQualifiedClassName, methodName, parameterTypes);
1116 }
1117
1118 static String getFullyQualifiedName(Element e) {
1119 return new SimpleElementVisitor14<String, Void>() {
1120 @Override
1121 public String visitModule(ModuleElement e, Void p) {
1122 return e.getQualifiedName().toString();
1123 }
1124
1125 @Override
1126 public String visitPackage(PackageElement e, Void p) {
1127 return e.getQualifiedName().toString();
1128 }
1129
1130 @Override
1131 public String visitType(TypeElement e, Void p) {
1132 return e.getQualifiedName().toString();
1133 }
1134
1135 @Override
1136 protected String defaultAction(Element e, Void p) {
1137 return visit(e.getEnclosingElement()) + "." + e.getSimpleName();
1138 }
1139 }.visit(e);
1140 }
1141
1142 static String getFullyQualifiedName(TypeMirror e) {
1143 return new SimpleTypeVisitor14<String, Void>() {
1144 @Override
1145 public String visitDeclared(DeclaredType t, Void p) {
1146 Element e = t.asElement();
1147 if (e instanceof TypeElement typeElement) {
1148 return typeElement.getQualifiedName().toString();
1149 }
1150 return super.visitDeclared(t, p);
1151 }
1152
1153 @Override
1154 public String visitPrimitive(PrimitiveType t, Void p) {
1155 return t.toString();
1156 }
1157
1158 @Override
1159 protected String defaultAction(TypeMirror e, Void p) {
1160 return e.toString();
1161 }
1162 }.visit(e);
1163 }
1164
1165 private ExecutableElement getReferencedExecutableElement(DocTreePath path, LinkTree link) {
1166 DocTreePath linkRefPath = DocTreePath.getPath(path, link.getReference());
1167 if (linkRefPath == null) {
1168 throw new DocTreePathAwareRuntimeException(
1169 path,
1170 "Could not resolve link reference: " + link.getReference().getSignature());
1171 }
1172 Element element = docTrees.getElement(linkRefPath);
1173 if (element instanceof ExecutableElement ee) {
1174 return ee;
1175 } else {
1176 throw new DocTreePathAwareRuntimeException(
1177 linkRefPath, "Expected an executable element, but got: " + element);
1178 }
1179 }
1180
1181 private static String toYesNo(boolean value) {
1182 return value ? "Yes" : "No";
1183 }
1184
1185 private static URI parseJavadocUrl(String value) {
1186 URI uri = URI.create(value);
1187 if (uri.getQuery() != null || uri.getFragment() != null) {
1188 throw new IllegalArgumentException("Javadoc base URL must not contain a query or fragment: " + value);
1189 }
1190 return value.endsWith("/") ? uri : URI.create(value + "/");
1191 }
1192
1193
1194
1195
1196 private static final class SingleArgumentOption implements Option {
1197 private final List<String> names;
1198 private final String description;
1199 private final String parameters;
1200 private final java.util.function.Consumer<String> processor;
1201
1202 SingleArgumentOption(
1203 List<String> names,
1204 String description,
1205 String parameters,
1206 java.util.function.Consumer<String> processor) {
1207 this.names = names;
1208 this.description = description;
1209 this.parameters = parameters;
1210 this.processor = processor;
1211 }
1212
1213 @Override
1214 public int getArgumentCount() {
1215 return 1;
1216 }
1217
1218 @Override
1219 public String getDescription() {
1220 return description;
1221 }
1222
1223 @Override
1224 public Kind getKind() {
1225 return Kind.STANDARD;
1226 }
1227
1228 @Override
1229 public List<String> getNames() {
1230 return names;
1231 }
1232
1233 @Override
1234 public String getParameters() {
1235 return parameters;
1236 }
1237
1238 @Override
1239 public boolean process(String option, List<String> arguments) {
1240 processor.accept(arguments.get(0));
1241
1242
1243 return true;
1244 }
1245 }
1246 }