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.SeeTree;
72 import com.sun.source.doctree.SinceTree;
73 import com.sun.source.doctree.SystemPropertyTree;
74 import com.sun.source.doctree.TextTree;
75 import com.sun.source.doctree.UnknownBlockTagTree;
76 import com.sun.source.doctree.ValueTree;
77 import com.sun.source.tree.ExpressionTree;
78 import com.sun.source.tree.IdentifierTree;
79 import com.sun.source.tree.MemberSelectTree;
80 import com.sun.source.tree.VariableTree;
81 import com.sun.source.util.DocTreePath;
82 import com.sun.source.util.DocTrees;
83 import com.sun.source.util.SimpleDocTreeVisitor;
84 import jdk.javadoc.doclet.Doclet;
85 import jdk.javadoc.doclet.DocletEnvironment;
86 import jdk.javadoc.doclet.Reporter;
87 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference;
88 import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
89 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105 public class ConfigurationCollectorDoclet implements Doclet {
106
107
108
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
145 String deprecated,
146
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
159
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
243
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
265
266 if (field.getConstantValue() == null) {
267 continue;
268 }
269 DocCommentTree docComment = docTrees.getDocCommentTree(field);
270 if (docComment == null) {
271
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
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
312
313
314
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
323 PrintWriter pw = reporter.getDiagnosticWriter();
324 if (pw == null) {
325 pw = new PrintWriter(System.err);
326 }
327 throwable.printStackTrace(pw);
328 }
329
330
331
332
333
334
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
346
347
348
349
350 private void reportError(String message, Throwable throwable) {
351 reporter.print(Diagnostic.Kind.ERROR, message);
352 reportError(throwable);
353 }
354
355
356
357
358
359
360 private void reportError(String message) {
361 reporter.print(Diagnostic.Kind.ERROR, message);
362 }
363
364
365
366
367
368
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
397 if (element.getAnnotation(Deprecated.class) == null) {
398
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
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
446
447
448
449
450
451
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
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
586
587 VariableElement referenced = resolveReferencedField(linkTreePath, link.getReference());
588 String value = referenced != null ? lookupConstant(referenced) : null;
589 if (value == null) {
590
591
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
601
602 return Optional.of(renderContent(defaultValuePath, RenderMode.PLAIN, true));
603 }
604
605
606
607
608
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
629 if (field.getKind() == ElementKind.ENUM_CONSTANT) {
630 return field.getSimpleName().toString();
631 }
632
633
634 return resolveEnumReference(field);
635 }
636
637
638
639
640
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
650 enumConstant = memberSelectTree.getIdentifier().toString();
651 } else if (initializer instanceof IdentifierTree identifierTree) {
652
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
672
673
674
675
676 private String getType(DocTreePath path, LinkTree link) {
677 String signature = link.getReference().getSignature();
678 if (signature.contains("#")) {
679
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
686
687 return resolveReferencedType(path, link.getReference())
688 .map(t -> t.getQualifiedName().toString())
689 .orElse(signature);
690 }
691
692
693
694
695
696
697 private Optional<TypeElement> resolveReferencedType(DocTreePath path, ReferenceTree reference) {
698
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
709 PLAIN,
710
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
720
721
722
723
724
725
726
727
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
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
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
830
831
832
833
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
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
859
860
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
873
874
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
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
911 return getSince(element.getEnclosingElement());
912 }
913
914 private String getSinceTag(DocTreePath path) {
915 if (path == null) {
916
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
1123
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
1141
1142
1143
1144
1145
1146
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
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
1286
1287 return true;
1288 }
1289 }
1290 }