diff --git a/.github/workflows/ci-actions-incremental.yml b/.github/workflows/ci-actions-incremental.yml index c23be01096424..5a9ad8ce63229 100644 --- a/.github/workflows/ci-actions-incremental.yml +++ b/.github/workflows/ci-actions-incremental.yml @@ -435,6 +435,16 @@ jobs: path: | build-reports.zip retention-days: 7 + - name: Upload test-produced debug dumps + uses: actions/upload-artifact@v4 + # We need this as soon as there's a matching file + # -- even in case of success, as some flaky tests won't fail the build + if: always() + with: + name: "debug-${{ github.run_attempt }}-JVM Tests - JDK ${{matrix.java.name}}" + path: "**/target/debug/**" + if-no-files-found: ignore # If we're not currently debugging any test, it's fine. + retention-days: 28 # We don't get notified for flaky tests, so let's give maintainers time to get back to it - name: Upload build.log (if build failed) uses: actions/upload-artifact@v4 if: ${{ failure() || cancelled() }} diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index f6ce1fcded42d..73645263b000b 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -2,7 +2,7 @@ com.gradle develocity-maven-extension - 1.22 + 1.22.1 com.gradle diff --git a/bom/application/pom.xml b/bom/application/pom.xml index d6a4b196a23e8..c078597d887f5 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -211,7 +211,7 @@ 6.10.0.202406032230-r 0.15.0 - 9.40 + 9.41 0.9.6 0.0.6 0.1.3 diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml index 9c0a6e8697281..e4ebd64ee2f98 100644 --- a/bom/dev-ui/pom.xml +++ b/bom/dev-ui/pom.xml @@ -13,7 +13,7 @@ Dependency management for dev-ui. Importable by third party extension developers. - 24.4.7 + 24.4.8 3.2.0 4.1.0 3.2.0 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java index f8cfaf031dfb2..6f3547172cc3b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java @@ -32,7 +32,7 @@ public GeneratedClassGizmoAdaptor(BuildProducer generat Predicate applicationClassPredicate) { this.generatedClasses = generatedClasses; this.applicationClassPredicate = applicationClassPredicate; - this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; } public GeneratedClassGizmoAdaptor(BuildProducer generatedClasses, @@ -44,7 +44,7 @@ public boolean test(String s) { return isApplicationClass(generatedToBaseNameFunction.apply(s)); } }; - this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; } @Override diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index 27b73b210bbef..6f0f4bce6ea97 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -46,6 +46,7 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.TestTag; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.reporting.ReportEntry; @@ -258,8 +259,9 @@ public void executionSkipped(TestIdentifier testIdentifier, String reason) { if (testClass != null) { Map results = resultsByClass.computeIfAbsent(testClass.getName(), s -> new HashMap<>()); - TestResult result = new TestResult(displayName, testClass.getName(), id, - TestExecutionResult.aborted(null), + TestResult result = new TestResult(displayName, testClass.getName(), + toTagList(testIdentifier), + id, TestExecutionResult.aborted(null), logHandler.captureOutput(), testIdentifier.isTest(), runId, 0, true); results.put(id, result); if (result.isTest()) { @@ -312,8 +314,9 @@ public void executionFinished(TestIdentifier testIdentifier, } Map results = resultsByClass.computeIfAbsent(testClassName, s -> new HashMap<>()); - TestResult result = new TestResult(displayName, testClassName, id, - testExecutionResult, + TestResult result = new TestResult(displayName, testClassName, + toTagList(testIdentifier), + id, testExecutionResult, logHandler.captureOutput(), testIdentifier.isTest(), runId, System.currentTimeMillis() - startTimes.get(testIdentifier), true); if (!results.containsKey(id)) { @@ -332,6 +335,7 @@ public void executionFinished(TestIdentifier testIdentifier, results.put(id, new TestResult(currentNonDynamicTest.get().getDisplayName(), result.getTestClass(), + toTagList(testIdentifier), currentNonDynamicTest.get().getUniqueIdObject(), TestExecutionResult.failed(failure), List.of(), false, runId, 0, false)); @@ -349,6 +353,7 @@ public void executionFinished(TestIdentifier testIdentifier, for (TestIdentifier child : children) { UniqueId childId = UniqueId.parse(child.getUniqueId()); result = new TestResult(child.getDisplayName(), testClassName, + toTagList(testIdentifier), childId, testExecutionResult, logHandler.captureOutput(), child.isTest(), runId, @@ -419,6 +424,15 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e } } + private static List toTagList(TestIdentifier testIdentifier) { + return testIdentifier + .getTags() + .stream() + .map(TestTag::getName) + .sorted() + .toList(); + } + private Class getTestClassFromSource(Optional optionalTestSource) { if (optionalTestSource.isPresent()) { var testSource = optionalTestSource.get(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java index 4250d332492dc..4f5025391a0b3 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java @@ -11,6 +11,7 @@ public class TestResult { final String displayName; final String testClass; + final List tags; final UniqueId uniqueId; final TestExecutionResult testExecutionResult; final List logOutput; @@ -20,10 +21,12 @@ public class TestResult { final List problems; final boolean reportable; - public TestResult(String displayName, String testClass, UniqueId uniqueId, TestExecutionResult testExecutionResult, + public TestResult(String displayName, String testClass, List tags, UniqueId uniqueId, + TestExecutionResult testExecutionResult, List logOutput, boolean test, long runId, long time, boolean reportable) { this.displayName = displayName; this.testClass = testClass; + this.tags = tags; this.uniqueId = uniqueId; this.testExecutionResult = testExecutionResult; this.logOutput = logOutput; @@ -58,6 +61,10 @@ public String getTestClass() { return testClass; } + public List getTags() { + return tags; + } + public UniqueId getUniqueId() { return uniqueId; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java index b91ed1874d636..921cb5fc8993e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/ClassTransformingBuildStep.java @@ -369,8 +369,9 @@ private byte[] transformClass(String className, List> getSboms(List sbomBuildItems) } private void writeDebugSourceFile(BuildResult result) { - String debugSourcesDir = BootstrapDebug.DEBUG_SOURCES_DIR; + String debugSourcesDir = BootstrapDebug.debugSourcesDir(); if (debugSourcesDir != null) { for (GeneratedClassBuildItem i : result.consumeMulti(GeneratedClassBuildItem.class)) { try { diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java index a719484c237d1..8287573c246e8 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java @@ -392,9 +392,10 @@ private static Map extractGeneratedResources(BuildResult buildRe for (GeneratedClassBuildItem i : buildResult.consumeMulti(GeneratedClassBuildItem.class)) { if (i.isApplicationClass() == applicationClasses) { data.put(fromClassNameToResourceName(i.getName()), i.getClassData()); - if (BootstrapDebug.DEBUG_CLASSES_DIR != null) { + var debugClassesDir = BootstrapDebug.debugClassesDir(); + if (debugClassesDir != null) { try { - File debugPath = new File(BootstrapDebug.DEBUG_CLASSES_DIR); + File debugPath = new File(debugClassesDir); if (!debugPath.exists()) { debugPath.mkdir(); } @@ -409,7 +410,7 @@ private static Map extractGeneratedResources(BuildResult buildRe } } - String debugSourcesDir = BootstrapDebug.DEBUG_SOURCES_DIR; + String debugSourcesDir = BootstrapDebug.debugSourcesDir(); if (debugSourcesDir != null) { try { if (i.getSource() != null) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java index d0ad35940cf60..75c1e996e5ebd 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/ConfigDocExtensionProcessor.java @@ -71,7 +71,12 @@ public void finalizeProcessing() { Properties javadocProperties = new Properties(); for (Entry javadocElementEntry : configCollector.getJavadocElements().entrySet()) { - javadocProperties.put(javadocElementEntry.getKey(), javadocElementEntry.getValue().rawJavadoc()); + if (javadocElementEntry.getValue().description() == null + || javadocElementEntry.getValue().description().isBlank()) { + continue; + } + + javadocProperties.put(javadocElementEntry.getKey(), javadocElementEntry.getValue().description()); } utils.filer().write(Outputs.META_INF_QUARKUS_JAVADOC, javadocProperties); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java deleted file mode 100644 index 027f96a25db24..0000000000000 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.quarkus.annotation.processor.documentation.config.discovery; - -public enum JavadocFormat { - ASCIIDOC, - JAVADOC; -} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java index 287be0b9639f4..3e6860a917ebc 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadoc.java @@ -1,6 +1,8 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadoc(String description, String since, String deprecated, JavadocFormat originalFormat) { +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadoc(String description, JavadocFormat format, String since, String deprecated) { public static ParsedJavadoc empty() { return new ParsedJavadoc(null, null, null, null); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java index 5f56acacb9cb3..6d1cbbabc6cc1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ParsedJavadocSection.java @@ -1,8 +1,10 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadocSection(String title, String details, String deprecated) { +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public record ParsedJavadocSection(String title, String details, JavadocFormat format, String deprecated) { public static ParsedJavadocSection empty() { - return new ParsedJavadocSection(null, null, null); + return new ParsedJavadocSection(null, null, null, null); } } \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java index 01d845e500a40..c2a3b9bf223d0 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/ResolvedType.java @@ -70,7 +70,7 @@ public static ResolvedType makeMap(TypeMirror type, ResolvedType unwrappedResolv return new ResolvedType(type, unwrappedResolvedType.unwrappedType, unwrappedResolvedType.binaryName, unwrappedResolvedType.qualifiedName, unwrappedResolvedType.simplifiedName, unwrappedResolvedType.isPrimitive, - true, unwrappedResolvedType.isList, + true, false, unwrappedResolvedType.isOptional, unwrappedResolvedType.isDeclared, unwrappedResolvedType.isInterface, unwrappedResolvedType.isClass, unwrappedResolvedType.isEnum, unwrappedResolvedType.isDuration, unwrappedResolvedType.isConfigGroup); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index fd5a67640d0b3..5007f9ce71bc0 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java @@ -1,6 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.formatter; -import java.util.Optional; import java.util.regex.Pattern; import org.jsoup.Jsoup; @@ -9,29 +8,17 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.javadoc.Javadoc; -import com.github.javaparser.javadoc.JavadocBlockTag; -import com.github.javaparser.javadoc.JavadocBlockTag.Type; -import com.github.javaparser.javadoc.description.JavadocDescription; import com.github.javaparser.javadoc.description.JavadocDescriptionElement; import com.github.javaparser.javadoc.description.JavadocInlineTag; -import io.quarkus.annotation.processor.documentation.config.discovery.JavadocFormat; -import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; -import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; import io.quarkus.annotation.processor.documentation.config.util.ConfigNamingUtil; public final class JavadocToAsciidocTransformer { - public static final JavadocToAsciidocTransformer INSTANCE = new JavadocToAsciidocTransformer(); - public static final JavadocToAsciidocTransformer INLINE_MACRO_INSTANCE = new JavadocToAsciidocTransformer(true); - private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); - private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); - private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); private static final Pattern STARTING_SPACE = Pattern.compile("^ +"); - private static final String DOT = "."; - private static final String BACKTICK = "`"; private static final String HASH = "#"; private static final String STAR = "*"; @@ -75,126 +62,31 @@ public final class JavadocToAsciidocTransformer { private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE = "[quote]\n____"; private static final String BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END = "____"; - private final boolean inlineMacroMode; - - private JavadocToAsciidocTransformer(boolean inlineMacroMode) { - this.inlineMacroMode = inlineMacroMode; - } - private JavadocToAsciidocTransformer() { - this(false); } - public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { - if (rawJavadoc == null || rawJavadoc.isBlank()) { - return ParsedJavadoc.empty(); - } - - // the parser expects all the lines to start with "* " - // we add it as it has been previously removed - Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); - - String description; - JavadocFormat originalFormat; + public static String toAsciidoc(String javadoc, JavadocFormat format) { + return toAsciidoc(javadoc, format, false); + } - if (isAsciidoc(javadoc)) { - description = handleEolInAsciidoc(javadoc); - originalFormat = JavadocFormat.ASCIIDOC; - } else { - description = htmlJavadocToAsciidoc(javadoc.getDescription()); - originalFormat = JavadocFormat.JAVADOC; + public static String toAsciidoc(String javadoc, JavadocFormat format, boolean inlineMacroMode) { + if (javadoc == null || javadoc.isBlank()) { + return null; } - Optional since = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.SINCE) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); - - Optional deprecated = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.DEPRECATED) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); - - if (description != null && description.isBlank()) { - description = null; - } - - return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalFormat); - } - - public ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { - if (javadocComment == null || javadocComment.trim().isEmpty()) { - return ParsedJavadocSection.empty(); + if (format == JavadocFormat.ASCIIDOC) { + return javadoc; + } else if (format == JavadocFormat.MARKDOWN) { + throw new IllegalArgumentException("Conversion from Markdown to Asciidoc is not supported"); } // the parser expects all the lines to start with "* " // we add it as it has been previously removed - javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); - Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); - Optional deprecated = javadoc.getBlockTags().stream() - .filter(t -> t.getType() == Type.DEPRECATED) - .map(JavadocBlockTag::getContent) - .map(JavadocDescription::toText) - .findFirst(); - - String asciidoc; - if (isAsciidoc(javadoc)) { - asciidoc = handleEolInAsciidoc(javadoc); - } else { - asciidoc = htmlJavadocToAsciidoc(javadoc.getDescription()); - } - - if (asciidoc == null || asciidoc.isBlank()) { - return ParsedJavadocSection.empty(); - } - - final int newLineIndex = asciidoc.indexOf(NEW_LINE); - final int dotIndex = asciidoc.indexOf(DOT); - - final int endOfTitleIndex; - if (newLineIndex > 0 && newLineIndex < dotIndex) { - endOfTitleIndex = newLineIndex; - } else { - endOfTitleIndex = dotIndex; - } - - if (endOfTitleIndex == -1) { - final String title = asciidoc.replaceAll("^([^\\w])+", "").trim(); - - return new ParsedJavadocSection(title, null, deprecated.orElse(null)); - } else { - final String title = asciidoc.substring(0, endOfTitleIndex).replaceAll("^([^\\w])+", "").trim(); - final String details = asciidoc.substring(endOfTitleIndex + 1).trim(); - - return new ParsedJavadocSection(title, details.isBlank() ? null : details, deprecated.orElse(null)); - } - } - - private String handleEolInAsciidoc(Javadoc javadoc) { - // it's Asciidoc, so we just pass through - // it also uses platform specific EOL, so we need to convert them back to \n - String asciidoc = javadoc.getDescription().toText(); - asciidoc = REPLACE_WINDOWS_EOL.matcher(asciidoc).replaceAll("\n"); - asciidoc = REPLACE_MACOS_EOL.matcher(asciidoc).replaceAll("\n"); - return asciidoc; - } - - private boolean isAsciidoc(Javadoc javadoc) { - for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { - if ("asciidoclet".equals(blockTag.getTagName())) { - return true; - } - } - return false; - } - - private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { StringBuilder sb = new StringBuilder(); - for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + for (JavadocDescriptionElement javadocDescriptionElement : parsedJavadoc.getDescription().getElements()) { if (javadocDescriptionElement instanceof JavadocInlineTag) { JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; String content = inlineTag.getContent().trim(); @@ -204,7 +96,7 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { case LITERAL: case SYSTEM_PROPERTY: sb.append('`'); - appendEscapedAsciiDoc(sb, content); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); break; case LINK: @@ -213,7 +105,7 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { content = ConfigNamingUtil.hyphenate(content.substring(1)); } sb.append('`'); - appendEscapedAsciiDoc(sb, content); + appendEscapedAsciiDoc(sb, content, inlineMacroMode); sb.append('`'); break; default: @@ -221,20 +113,22 @@ private String htmlJavadocToAsciidoc(JavadocDescription javadocDescription) { break; } } else { - appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText())); + appendHtml(sb, Jsoup.parseBodyFragment(javadocDescriptionElement.toText()), inlineMacroMode); } } - return trim(sb); + String asciidoc = trim(sb); + + return asciidoc.isBlank() ? null : asciidoc; } - private void appendHtml(StringBuilder sb, Node node) { + private static void appendHtml(StringBuilder sb, Node node, boolean inlineMacroMode) { for (Node childNode : node.childNodes()) { switch (childNode.nodeName()) { case PARAGRAPH_NODE: newLine(sb); newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case PREFORMATED_NODE: newLine(sb); @@ -254,7 +148,7 @@ private void appendHtml(StringBuilder sb, Node node) { newLine(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE); newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); newLineIfNeeded(sb); sb.append(BLOCKQUOTE_BLOCK_ASCIDOC_STYLE_END); newLine(sb); @@ -263,7 +157,7 @@ private void appendHtml(StringBuilder sb, Node node) { case ORDERED_LIST_NODE: case UN_ORDERED_LIST_NODE: newLine(sb); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case LIST_ITEM_NODE: final String marker = childNode.parent().nodeName().equals(ORDERED_LIST_NODE) @@ -271,59 +165,59 @@ private void appendHtml(StringBuilder sb, Node node) { : UNORDERED_LIST_ITEM_ASCIDOC_STYLE; newLine(sb); sb.append(marker); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; case LINK_NODE: final String link = childNode.attr(HREF_ATTRIBUTE); sb.append("link:"); sb.append(link); final StringBuilder caption = new StringBuilder(); - appendHtml(caption, childNode); + appendHtml(caption, childNode, inlineMacroMode); sb.append(String.format(LINK_ATTRIBUTE_FORMAT, trim(caption))); break; case CODE_NODE: sb.append(BACKTICK); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(BACKTICK); break; case BOLD_NODE: case STRONG_NODE: sb.append(STAR); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(STAR); break; case EMPHASIS_NODE: case ITALICS_NODE: sb.append(UNDERSCORE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(UNDERSCORE); break; case UNDERLINE_NODE: sb.append(UNDERLINE_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SMALL_NODE: sb.append(SMALL_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case BIG_NODE: sb.append(BIG_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case SUB_SCRIPT_NODE: sb.append(SUB_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(SUB_SCRIPT_ASCIDOC_STYLE); break; case SUPER_SCRIPT_NODE: sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(SUPER_SCRIPT_ASCIDOC_STYLE); break; case DEL_NODE: @@ -331,7 +225,7 @@ private void appendHtml(StringBuilder sb, Node node) { case STRIKE_NODE: sb.append(LINE_THROUGH_ASCIDOC_STYLE); sb.append(HASH); - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); sb.append(HASH); break; case NEW_LINE_NODE: @@ -352,10 +246,10 @@ private void appendHtml(StringBuilder sb, Node node) { text = startingSpaceMatcher.replaceFirst(""); } - appendEscapedAsciiDoc(sb, text); + appendEscapedAsciiDoc(sb, text, inlineMacroMode); break; default: - appendHtml(sb, childNode); + appendHtml(sb, childNode, inlineMacroMode); break; } } @@ -431,7 +325,7 @@ private static StringBuilder trimText(StringBuilder sb, String charsToTrim) { return sb; } - private StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { + private static StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { int i = 0; /* trim leading whitespace */ LOOP: while (i < text.length()) { @@ -497,7 +391,7 @@ private StringBuilder unescapeHtmlEntities(StringBuilder sb, String text) { return sb; } - private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { + private static StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text, boolean inlineMacroMode) { boolean escaping = false; for (int i = 0; i < text.length(); i++) { final char ch = text.charAt(i); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java new file mode 100644 index 0000000000000..fd5796ed7b8b0 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java @@ -0,0 +1,87 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import java.util.regex.Pattern; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.description.JavadocDescription; +import com.github.javaparser.javadoc.description.JavadocDescriptionElement; +import com.github.javaparser.javadoc.description.JavadocInlineTag; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public class JavadocToMarkdownTransformer { + + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + + public static String toMarkdown(String javadoc, JavadocFormat format) { + if (javadoc == null || javadoc.isBlank()) { + return null; + } + + if (format == JavadocFormat.MARKDOWN) { + return javadoc; + } else if (format == JavadocFormat.JAVADOC) { + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc parsedJavadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(javadoc).replaceAll("* ")); + + // HTML is valid Javadoc but we need to drop the Javadoc tags e.g. {@link ...} + return simplifyJavadoc(parsedJavadoc.getDescription()); + } + + // it's Asciidoc, the fun begins... + return ""; + } + + /** + * This is not definitely not perfect but it can be used to filter the Javadoc included in Markdown. + *

+ * We will need to discuss further how to handle passing the Javadoc to the IDE. + * In Quarkus, we have Asciidoc, standard Javadoc and soon we might have Markdown javadoc. + */ + private static String simplifyJavadoc(JavadocDescription javadocDescription) { + StringBuilder sb = new StringBuilder(); + + for (JavadocDescriptionElement javadocDescriptionElement : javadocDescription.getElements()) { + if (javadocDescriptionElement instanceof JavadocInlineTag) { + JavadocInlineTag inlineTag = (JavadocInlineTag) javadocDescriptionElement; + String content = inlineTag.getContent().trim(); + switch (inlineTag.getType()) { + case CODE: + case VALUE: + case LITERAL: + case SYSTEM_PROPERTY: + case LINK: + case LINKPLAIN: + sb.append(""); + sb.append(escapeHtml(content)); + sb.append(""); + break; + default: + sb.append(content); + break; + } + } else { + sb.append(javadocDescriptionElement.toText()); + } + } + + return sb.toString().trim(); + } + + private static String escapeHtml(String s) { + StringBuilder out = new StringBuilder(Math.max(16, s.length())); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c > 127 || c == '"' || c == '\'' || c == '<' || c == '>' || c == '&') { + out.append("&#"); + out.append((int) c); + out.append(';'); + } else { + out.append(c); + } + } + return out.toString(); + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java new file mode 100644 index 0000000000000..59da647afdfa3 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java @@ -0,0 +1,20 @@ +package io.quarkus.annotation.processor.documentation.config.formatter; + +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +public final class JavadocTransformer { + + private JavadocTransformer() { + } + + public static String transform(String javadoc, JavadocFormat fromFormat, JavadocFormat toFormat) { + switch (toFormat) { + case ASCIIDOC: + return JavadocToAsciidocTransformer.toAsciidoc(javadoc, fromFormat); + case MARKDOWN: + return JavadocToMarkdownTransformer.toMarkdown(javadoc, fromFormat); + default: + throw new IllegalArgumentException("Converting to " + toFormat + " is not supported"); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java index 01527fbbc4125..77cd8dd3f1e53 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigItemCollection.java @@ -17,6 +17,20 @@ default List getNonDeprecatedItems() { .toList(); } + @JsonIgnore + default List getNonDeprecatedProperties() { + return getItems().stream() + .filter(i -> i instanceof ConfigProperty && !i.isDeprecated()) + .toList(); + } + + @JsonIgnore + default List getNonDeprecatedSections() { + return getItems().stream() + .filter(i -> i instanceof ConfigSection && !i.isDeprecated()) + .toList(); + } + void addItem(AbstractConfigItem item); boolean hasDurationType(); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java index 9ef41190a3268..8a3db353d4c9f 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java @@ -2,11 +2,9 @@ import java.util.Map; -import com.fasterxml.jackson.annotation.JsonIgnore; - public record JavadocElements(Extension extension, Map elements) { - public record JavadocElement(String description, String since, String deprecated, @JsonIgnore String rawJavadoc) { + public record JavadocElement(String description, JavadocFormat format, String since, String deprecated) { } public boolean isEmpty() { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java new file mode 100644 index 0000000000000..b44020aa4ebb1 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java @@ -0,0 +1,8 @@ +package io.quarkus.annotation.processor.documentation.config.model; + +public enum JavadocFormat { + + ASCIIDOC, + MARKDOWN, + JAVADOC; +} \ No newline at end of file diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java index 490866047cf7d..a2fe0e875223a 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/AbstractJavadocConfigListener.java @@ -9,8 +9,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryConfigRoot; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.util.Config; import io.quarkus.annotation.processor.util.Utils; @@ -41,13 +41,15 @@ public Optional onConfigRoot(TypeElement configRoot) { return Optional.empty(); } - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc.get()); + if (parsedJavadocSection.title() == null) { + return Optional.empty(); + } configCollector.addJavadocElement( configRoot.getQualifiedName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); return Optional.empty(); } @@ -69,13 +71,17 @@ public void onResolvedEnum(TypeElement enumTypeElement) { continue; } - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc.get()); + + if (parsedJavadoc.description() == null) { + continue; + } configCollector.addJavadocElement( enumTypeElement.getQualifiedName().toString() + Markers.DOT + enumElement.getSimpleName() .toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java index 56ec7c8dc11f4..3a978fd690ea9 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java @@ -1,7 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.scanner; -import java.util.Optional; - import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -9,8 +7,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.annotation.processor.util.Config; @@ -38,33 +36,36 @@ public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElem return; } - Optional rawJavadoc = utils.element().getJavadoc(method); + String rawJavadoc = utils.element().getJavadoc(method).orElse(""); boolean isSection = utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_SECTION); - if (rawJavadoc.isEmpty()) { - // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() || isSection) { - utils.element().addMissingJavadocError(method); - } - return; - } - if (isSection) { // for sections, we only keep the title - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); } else { - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(method); + } + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java index bfbc991d0ff28..638a7bfb4ed4b 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java @@ -1,7 +1,5 @@ package io.quarkus.annotation.processor.documentation.config.scanner; -import java.util.Optional; - import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -9,8 +7,8 @@ import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; -import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.annotation.processor.documentation.config.util.Types; import io.quarkus.annotation.processor.util.Config; @@ -38,33 +36,36 @@ public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeEleme return; } - Optional rawJavadoc = utils.element().getJavadoc(field); + String rawJavadoc = utils.element().getJavadoc(field).orElse(""); boolean isSection = utils.element().isAnnotationPresent(field, Types.ANNOTATION_CONFIG_DOC_SECTION); - if (rawJavadoc.isEmpty()) { - // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() || isSection) { - utils.element().addMissingJavadocError(field); - } - return; - } - if (isSection) { // for sections, we only keep the title - ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(rawJavadoc.get()); + ParsedJavadocSection parsedJavadocSection = JavadocUtil.parseConfigSectionJavadoc(rawJavadoc); + + if (parsedJavadocSection.title() == null) { + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), - new JavadocElement(parsedJavadocSection.title(), null, parsedJavadocSection.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadocSection.title(), parsedJavadocSection.format(), null, + parsedJavadocSection.deprecated())); } else { - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + ParsedJavadoc parsedJavadoc = JavadocUtil.parseConfigItemJavadoc(rawJavadoc); + + // We require a Javadoc for config items that are not config groups except if they are a section + if (parsedJavadoc.description() == null) { + if (parsedJavadoc.deprecated() == null && !resolvedType.isConfigGroup()) { + utils.element().addMissingJavadocError(field); + } + return; + } configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.format(), parsedJavadoc.since(), + parsedJavadoc.deprecated())); } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java index 8ca9e307b0ca0..2bed8914e4cb0 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java @@ -2,12 +2,31 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jsoup.Jsoup; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.javadoc.Javadoc; +import com.github.javaparser.javadoc.JavadocBlockTag; +import com.github.javaparser.javadoc.JavadocBlockTag.Type; +import com.github.javaparser.javadoc.description.JavadocDescription; + +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + public final class JavadocUtil { + private static final Pattern START_OF_LINE = Pattern.compile("^", Pattern.MULTILINE); + private static final Pattern REPLACE_WINDOWS_EOL = Pattern.compile("\r\n"); + private static final Pattern REPLACE_MACOS_EOL = Pattern.compile("\r"); + private static final String DOT = "."; + private static final String NEW_LINE = "\n"; + static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; static final String OFFICIAL_JAVA_DOC_BASE_LINK = "https://docs.oracle.com/en/java/javase/17/docs/api/java.base/"; static final String AGROAL_API_JAVA_DOC_SITE = "https://javadoc.io/doc/io.agroal/agroal-api/latest/"; @@ -25,6 +44,118 @@ public final class JavadocUtil { private JavadocUtil() { } + public static ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { + if (rawJavadoc == null || rawJavadoc.isBlank()) { + return ParsedJavadoc.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + Optional since = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.SINCE) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + if (description != null && description.isBlank()) { + description = null; + } + + return new ParsedJavadoc(description, format, since.orElse(null), deprecated.orElse(null)); + } + + public static ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { + if (javadocComment == null || javadocComment.trim().isEmpty()) { + return ParsedJavadocSection.empty(); + } + + // the parser expects all the lines to start with "* " + // we add it as it has been previously removed + javadocComment = START_OF_LINE.matcher(javadocComment).replaceAll("* "); + Javadoc javadoc = StaticJavaParser.parseJavadoc(javadocComment); + + Optional deprecated = javadoc.getBlockTags().stream() + .filter(t -> t.getType() == Type.DEPRECATED) + .map(JavadocBlockTag::getContent) + .map(JavadocDescription::toText) + .findFirst(); + + String description; + JavadocFormat format; + + if (isAsciidoc(javadoc)) { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.ASCIIDOC; + } else if (isMarkdown(javadoc)) { + // this is to prepare the Markdown Javadoc that will come up soon enough + // I don't know exactly how the parser will deal with them though + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.MARKDOWN; + } else { + description = normalizeEol(javadoc.getDescription().toText()); + format = JavadocFormat.JAVADOC; + } + + if (description == null || description.isBlank()) { + return ParsedJavadocSection.empty(); + } + + final int newLineIndex = description.indexOf(NEW_LINE); + final int dotIndex = description.indexOf(DOT); + + final int endOfTitleIndex; + if (newLineIndex > 0 && newLineIndex < dotIndex) { + endOfTitleIndex = newLineIndex; + } else { + endOfTitleIndex = dotIndex; + } + + String title; + String details; + + if (endOfTitleIndex == -1) { + title = description.trim(); + details = null; + } else { + title = description.substring(0, endOfTitleIndex).trim(); + details = description.substring(endOfTitleIndex + 1).trim(); + } + + if (title.contains("<")) { + title = Jsoup.parse(title).text(); + } + + title = title.replaceAll("^([^\\w])+", ""); + + return new ParsedJavadocSection(title == null || title.isBlank() ? null : title, + details == null || details.isBlank() ? null : details, format, + deprecated.orElse(null)); + } + /** * Get javadoc link of a given type value */ @@ -59,6 +190,33 @@ public static String getJavadocSiteLink(String binaryName) { return null; } + private static String normalizeEol(String javadoc) { + // it's Asciidoc, so we just pass through + // it also uses platform specific EOL, so we need to convert them back to \n + String normalizedJavadoc = javadoc; + normalizedJavadoc = REPLACE_WINDOWS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + normalizedJavadoc = REPLACE_MACOS_EOL.matcher(normalizedJavadoc).replaceAll("\n"); + return normalizedJavadoc; + } + + private static boolean isAsciidoc(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("asciidoclet".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + + private static boolean isMarkdown(Javadoc javadoc) { + for (JavadocBlockTag blockTag : javadoc.getBlockTags()) { + if ("markdown".equals(blockTag.getTagName())) { + return true; + } + } + return false; + } + private static String getJavaDocLinkForType(String type) { int beginOfWrappedTypeIndex = type.indexOf("<"); if (beginOfWrappedTypeIndex != -1) { diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java index eaf41d1ec52a5..86d07a9229ef1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/ElementUtil.java @@ -172,9 +172,10 @@ public Optional getJavadoc(Element e) { } public void addMissingJavadocError(Element e) { - processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, - "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e, e); + String error = "Unable to find javadoc for config item " + e.getEnclosingElement() + " " + e; + + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, error, e); + throw new IllegalStateException(error); } public boolean isJdkClass(TypeElement e) { diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java index 3bab12d00c215..546e8cc05b11f 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigItemTest.java @@ -11,51 +11,42 @@ import org.junit.jupiter.params.provider.ValueSource; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; public class JavadocToAsciidocTransformerConfigItemTest { - @Test - public void parseNullJavaDoc() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(null); - assertNull(parsed.description()); - } - @Test public void removeParagraphIndentation() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE - .parseConfigItemJavadoc("First paragraph

Second Paragraph"); - assertEquals("First paragraph +\n +\nSecond Paragraph", parsed.description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("First paragraph

Second Paragraph"); + assertEquals("First paragraph +\n +\nSecond Paragraph", + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @Test public void parseUntrimmedJavaDoc() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(" "); - assertNull(parsed.description()); - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("

"); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(" "); assertNull(parsed.description()); - } - - @Test - public void parseSimpleJavaDoc() { - String javaDoc = "hello world"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(javaDoc, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc("

"); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertNull(description); } @Test public void parseJavaDocWithParagraph() { String javaDoc = "hello

world

"; String expectedOutput = "hello\n\nworld"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); javaDoc = "hello world

bonjour

le monde

"; expectedOutput = "hello world\n\nbonjour\n\nle monde"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test @@ -63,55 +54,64 @@ public void parseJavaDocWithStyles() { // Bold String javaDoc = "hello world"; String expectedOutput = "hello *world*"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); javaDoc = "hello world"; expectedOutput = "hello *world*"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Emphasized javaDoc = "hello world"; expectedOutput = "_hello world_"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Italics javaDoc = "hello world"; expectedOutput = "_hello world_"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // Underline javaDoc = "hello world"; expectedOutput = "[.underline]#hello world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // small javaDoc = "quarkus subatomic"; expectedOutput = "[.small]#quarkus subatomic#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // big javaDoc = "hello world"; expectedOutput = "[.big]#hello world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // line through javaDoc = "hello monolith world"; expectedOutput = "[.line-through]#hello #[.line-through]#monolith #[.line-through]#world#"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); // superscript and subscript javaDoc = "cloud in-premise"; expectedOutput = "^cloud ^~in-premise~"; - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); - assertEquals(expectedOutput, parsed.description()); + parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); + assertEquals(expectedOutput, description); } @Test @@ -123,9 +123,10 @@ public void parseJavaDocWithLiTagsInsideUlTag() { "" + ""; String expectedOutput = "List:\n\n - 1\n - 2"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test @@ -137,76 +138,93 @@ public void parseJavaDocWithLiTagsInsideOlTag() { "" + ""; String expectedOutput = "List:\n\n . 1\n . 2"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLinkInlineSnippet() { String javaDoc = "{@link firstlink} {@link #secondlink} \n {@linkplain #third.link}"; String expectedOutput = "`firstlink` `secondlink` `third.link`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLinkTag() { String javaDoc = "this is a hello link"; String expectedOutput = "this is a link:http://link.com[hello] link"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithCodeInlineSnippet() { String javaDoc = "{@code true} {@code false}"; String expectedOutput = "`true` `false`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithLiteralInlineSnippet() { String javaDoc = "{@literal java.util.Boolean}"; String expectedOutput = "`java.util.Boolean`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithValueInlineSnippet() { String javaDoc = "{@value 10s}"; String expectedOutput = "`10s`"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithUnknownInlineSnippet() { String javaDoc = "{@see java.util.Boolean}"; String expectedOutput = "java.util.Boolean"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithUnknownNode() { String javaDoc = "hello"; String expectedOutput = "hello"; - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + String description = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); - assertEquals(expectedOutput, parsed.description()); + assertEquals(expectedOutput, description); } @Test public void parseJavaDocWithBlockquoteBlock() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("See Section 4.5.5 of the JSR 380 specification, specifically\n" + + "\n" + + "
\n" + + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" + + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" + + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" + + "
\nThat was interesting, wasn't it?"); + assertEquals("See Section 4.5.5 of the JSR 380 specification, specifically\n" + "\n" + "[quote]\n" @@ -215,49 +233,37 @@ public void parseJavaDocWithBlockquoteBlock() { + "____\n" + "\n" + "That was interesting, wasn't it?", - JavadocToAsciidocTransformer.INSTANCE - .parseConfigItemJavadoc("See Section 4.5.5 of the JSR 380 specification, specifically\n" - + "\n" - + "
\n" - + "In sub types (be it sub classes/interfaces or interface implementations), no parameter constraints may\n" - + "be declared on overridden or implemented methods, nor may parameters be marked for cascaded validation.\n" - + "This would pose a strengthening of preconditions to be fulfilled by the caller.\n" - + "
\nThat was interesting, wasn't it?") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); assertEquals( "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); // TODO // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); } @Test public void parseJavaDocWithCodeBlock() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Example:\n\n
\nfoo\nbar\n
\n\nbaz"); + assertEquals("Example:\n\n```\nfoo\nbar\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
\nfoo\nbar\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); + + parsed = JavadocUtil.parseConfigItemJavadoc( + "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz"); assertEquals( "Some HTML entities & special characters:\n\n```\n|[/variant]|/[/variant]\n```\n\nbaz", - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc( - "Some HTML entities & special characters:\n\n
<os>|<arch>[/variant]|<os>/<arch>[/variant]\n
\n\nbaz") - .description()); + JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); // TODO // assertEquals("Example:\n\n```\nfoo\nbar\n```", - // JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); - } - - @Test - public void since() { - ParsedJavadoc parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc("Javadoc text\n\n@since 1.2.3"); - assertEquals("Javadoc text", parsed.description()); - assertEquals("1.2.3", parsed.since()); + // JavadocUtil.parseConfigItemJavadoc("Example:\n\n
{@code\nfoo\nbar\n}
")); } @Test @@ -276,8 +282,9 @@ public void asciidoc() { "And some code\n" + "----"; - assertEquals(asciidoc, - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet").description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @Test @@ -289,8 +296,9 @@ public void asciidocLists() { " * 1.2\n" + "* 2"; - assertEquals(asciidoc, - JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet").description()); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(asciidoc + "\n" + "@asciidoclet"); + + assertEquals(asciidoc, JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format())); } @ParameterizedTest @@ -299,7 +307,8 @@ public void escape(String ch) { final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); final String expected = "
\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", JavaDoc tag " + ch + " " + ch + ch + "

\n
"; @@ -312,8 +321,8 @@ public void escapeInsideInlineElement(String ch) { final String javaDoc = "Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + ", {@code JavaDoc tag " + ch + " " + ch + ch + "}"; - final String asciiDoc = JavadocToAsciidocTransformer.INLINE_MACRO_INSTANCE.parseConfigItemJavadoc(javaDoc) - .description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format(), true); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); if (ch.equals("]")) { @@ -329,7 +338,8 @@ public void escapePlus() { final String javaDoc = "Inline + ++, HTML tag glob + ++, {@code JavaDoc tag + ++}"; final String expected = "
\n

Inline + ++, HTML tag glob + ++, JavaDoc tag + ++

\n
"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); assertEquals(expected, actual); } @@ -342,7 +352,8 @@ public void escapeBrackets(String ch) { final String expected = "
\n

Inline " + ch + " " + ch + ch + ", HTML tag glob " + ch + " " + ch + ch + "

\n
"; - final String asciiDoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(javaDoc).description(); + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + final String asciiDoc = JavadocToAsciidocTransformer.toAsciidoc(parsed.description(), parsed.format()); final String actual = Factory.create().convert(asciiDoc, Collections.emptyMap()); assertEquals(expected, actual); } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java index 190b32c9bba84..9ab4290efa591 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformerConfigSectionTest.java @@ -5,25 +5,19 @@ import org.junit.jupiter.api.Test; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; +import io.quarkus.annotation.processor.documentation.config.util.JavadocUtil; public class JavadocToAsciidocTransformerConfigSectionTest { - @Test - public void parseNullSection() { - ParsedJavadocSection parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(null); - assertEquals(null, parsed.details()); - assertEquals(null, parsed.title()); - } - @Test public void parseUntrimmedJavaDoc() { - ParsedJavadocSection parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(" "); - assertEquals(null, parsed.title()); - assertEquals(null, parsed.details()); + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(" "); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); - parsed = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc("

"); - assertEquals(null, parsed.title()); - assertEquals(null, parsed.details()); + parsed = JavadocUtil.parseConfigSectionJavadoc("

"); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.title(), parsed.format())); + assertEquals(null, JavadocToAsciidocTransformer.toAsciidoc(parsed.details(), parsed.format())); } @Test @@ -43,10 +37,9 @@ public void passThroughAConfigSectionInAsciiDoc() { String asciidoc = "=== " + title + "\n\n" + details; - ParsedJavadocSection sectionHolder = JavadocToAsciidocTransformer.INSTANCE - .parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); - assertEquals(title, sectionHolder.title()); - assertEquals(details, sectionHolder.details()); + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals(title, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); + assertEquals(details, JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.details(), sectionHolder.format())); asciidoc = "Asciidoc title. \n" + "\n" + @@ -62,66 +55,7 @@ public void passThroughAConfigSectionInAsciiDoc() { "And some code\n" + "----"; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); - assertEquals("Asciidoc title", sectionHolder.title()); - } - - @Test - public void parseSectionWithoutIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section"; - String expectedTitle = "Config Section"; - String expectedDetails = null; - ParsedJavadocSection sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - - javaDoc = "Config Section."; - expectedTitle = "Config Section"; - expectedDetails = null; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - - /** - * html javadoc - */ - javaDoc = "

Config Section

"; - expectedTitle = "Config Section"; - expectedDetails = null; - sectionHolder = JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc); - assertEquals(expectedDetails, sectionHolder.details()); - assertEquals(expectedTitle, sectionHolder.title()); - } - - @Test - public void parseSectionWithIntroduction() { - /** - * Simple javadoc - */ - String javaDoc = "Config Section .Introduction"; - String expectedDetails = "Introduction"; - String expectedTitle = "Config Section"; - assertEquals(expectedTitle, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - - /** - * html javadoc - */ - javaDoc = "

Config Section

. Introduction"; - expectedDetails = "Introduction"; - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - assertEquals(expectedTitle, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); - } - - @Test - public void properlyParseConfigSectionWrittenInHtml() { - String javaDoc = "

Config Section.

This is section introduction"; - String expectedDetails = "This is section introduction"; - String title = "Config Section"; - assertEquals(expectedDetails, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).details()); - assertEquals(title, JavadocToAsciidocTransformer.INSTANCE.parseConfigSectionJavadoc(javaDoc).title()); + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(asciidoc + "\n" + "@asciidoclet"); + assertEquals("Asciidoc title", JavadocToAsciidocTransformer.toAsciidoc(sectionHolder.title(), sectionHolder.format())); } } diff --git a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java index 9fab0e55e0e52..1b16410465b84 100644 --- a/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java +++ b/core/processor/src/test/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtilTest.java @@ -11,8 +11,118 @@ import org.junit.jupiter.api.Test; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; + public class JavadocUtilTest { + @Test + public void parseNullJavaDoc() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(null); + assertNull(parsed.description()); + } + + @Test + public void parseSimpleJavaDoc() { + String javaDoc = "hello world"; + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc(javaDoc); + + assertEquals(javaDoc, parsed.description()); + } + + @Test + public void since() { + ParsedJavadoc parsed = JavadocUtil.parseConfigItemJavadoc("Javadoc text\n\n@since 1.2.3"); + assertEquals("Javadoc text", parsed.description()); + assertEquals("1.2.3", parsed.since()); + } + + @Test + public void deprecated() { + ParsedJavadoc parsed = JavadocUtil + .parseConfigItemJavadoc("@deprecated JNI is always enabled starting from GraalVM 19.3.1."); + assertEquals(null, parsed.description()); + } + + @Test + public void parseNullSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc(null); + assertEquals(null, parsed.details()); + assertEquals(null, parsed.title()); + } + + @Test + public void parseSimpleSection() { + ParsedJavadocSection parsed = JavadocUtil.parseConfigSectionJavadoc("title"); + assertEquals("title", parsed.title()); + assertEquals(null, parsed.details()); + } + + @Test + public void parseSectionWithIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section .Introduction"; + String expectedDetails = "Introduction"; + String expectedTitle = "Config Section"; + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

. Introduction"; + expectedDetails = "Introduction"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(expectedTitle, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithParagraph() { + String javaDoc = "Dev Services\n

\nDev Services allows Quarkus to automatically start Elasticsearch in dev and test mode."; + assertEquals("Dev Services", JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void properlyParseConfigSectionWrittenInHtml() { + String javaDoc = "Config Section.

This is section introduction"; + String expectedDetails = "

This is section introduction"; + String title = "Config Section"; + assertEquals(expectedDetails, JavadocUtil.parseConfigSectionJavadoc(javaDoc).details()); + assertEquals(title, JavadocUtil.parseConfigSectionJavadoc(javaDoc).title()); + } + + @Test + public void parseSectionWithoutIntroduction() { + /** + * Simple javadoc + */ + String javaDoc = "Config Section"; + String expectedTitle = "Config Section"; + String expectedDetails = null; + ParsedJavadocSection sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + javaDoc = "Config Section."; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + + /** + * html javadoc + */ + javaDoc = "

Config Section

"; + expectedTitle = "Config Section"; + expectedDetails = null; + sectionHolder = JavadocUtil.parseConfigSectionJavadoc(javaDoc); + assertEquals(expectedDetails, sectionHolder.details()); + assertEquals(expectedTitle, sectionHolder.title()); + } + @Test public void shouldReturnEmptyListForPrimitiveValue() { String value = JavadocUtil.getJavadocSiteLink("int"); diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java index c6c6386052cc8..a1ed3bf1bae0e 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java @@ -1,406 +1,13 @@ package io.quarkus.maven.config.doc; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import org.apache.maven.execution.MavenSession; -import org.apache.maven.plugin.AbstractMojo; -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; - -import io.quarkus.annotation.processor.documentation.config.merger.JavadocMerger; -import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; -import io.quarkus.annotation.processor.documentation.config.merger.MergedModel; -import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; -import io.quarkus.annotation.processor.documentation.config.merger.ModelMerger; -import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; -import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; -import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; -import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; -import io.quarkus.annotation.processor.documentation.config.model.Extension; -import io.quarkus.qute.Engine; -import io.quarkus.qute.ReflectionValueResolver; -import io.quarkus.qute.UserTagSectionHelper; -import io.quarkus.qute.ValueResolver; +/** + * We will recommend using generate-config-doc that is more flexible. + *

+ * The GenerateConfigDocMojo defaults have to stay consistent with this. + */ @Mojo(name = "generate-asciidoc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) -public class GenerateAsciidocMojo extends AbstractMojo { - - private static final String TARGET = "target"; - - private static final String ADOC_SUFFIX = ".adoc"; - private static final String CONFIG_ROOT_FILE_FORMAT = "%s_%s.adoc"; - private static final String EXTENSION_FILE_FORMAT = "%s.adoc"; - private static final String ALL_CONFIG_FILE_NAME = "quarkus-all-config.adoc"; - - @Parameter(defaultValue = "${session}", readonly = true) - private MavenSession mavenSession; - - @Parameter - private File scanDirectory; - - @Parameter(defaultValue = "${project.build.directory}/quarkus-generated-doc/config", required = true) - private File targetDirectory; - - @Parameter(defaultValue = "false") - private boolean generateAllConfig; - - @Parameter(defaultValue = "false") - private boolean enableEnumTooltips; - - @Parameter(defaultValue = "false") - private boolean skip; - - @Override - public void execute() throws MojoExecutionException, MojoFailureException { - if (skip) { - return; - } - - // I was unable to find an easy way to get the root directory of the project - Path resolvedScanDirectory = scanDirectory != null ? scanDirectory.toPath() - : mavenSession.getCurrentProject().getBasedir().toPath().getParent(); - Path resolvedTargetDirectory = targetDirectory.toPath(); - initTargetDirectory(resolvedTargetDirectory); - - List targetDirectories = findTargetDirectories(resolvedScanDirectory); - - JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); - MergedModel mergedModel = ModelMerger.mergeModel(javadocRepository, targetDirectories); - - AsciidocFormatter asciidocFormatter = new AsciidocFormatter(javadocRepository, enableEnumTooltips); - Engine quteEngine = initializeQuteEngine(asciidocFormatter); - - // we generate a file per extension + top level prefix - for (Entry> extensionConfigRootsEntry : mergedModel.getConfigRoots() - .entrySet()) { - Extension extension = extensionConfigRootsEntry.getKey(); - - Path configRootAdocPath = null; - - for (Entry configRootEntry : extensionConfigRootsEntry.getValue().entrySet()) { - String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); - ConfigRoot configRoot = configRootEntry.getValue(); - - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } - - configRootAdocPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, - extension.artifactId(), topLevelPrefix)); - String summaryTableId = asciidocFormatter - .toAnchor(extension.artifactId() + "_" + topLevelPrefix); - - try { - Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render config roots for top level prefix: " + topLevelPrefix - + " in extension: " + extension, e); - } - } - - // if we have only one top level prefix, we copy the generated file to a file named after the extension - // for simplicity's sake - if (extensionConfigRootsEntry.getValue().size() == 1 && configRootAdocPath != null) { - Path extensionAdocPath = resolvedTargetDirectory.resolve(String.format(EXTENSION_FILE_FORMAT, - extension.artifactId())); - - try { - Files.copy(configRootAdocPath, extensionAdocPath, StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new MojoExecutionException("Unable to copy extension file for: " + extension, e); - } - } - } - - // we generate the config roots that are saved in a specific file - for (Entry specificFileConfigRootEntry : mergedModel.getConfigRootsInSpecificFile().entrySet()) { - String fileName = specificFileConfigRootEntry.getKey(); - ConfigRoot configRoot = specificFileConfigRootEntry.getValue(); - - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } - - Extension extension = configRoot.getExtension(); - - if (!fileName.endsWith(".adoc")) { - fileName += ".adoc"; - } - - Path configRootAdocPath = resolvedTargetDirectory.resolve(fileName); - String summaryTableId = asciidocFormatter.toAnchor(stripAdocSuffix(fileName)); - - try { - Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render config roots for specific file: " + fileName - + " in extension: " + extension, e); - } - } - - // we generate files for generated sections - for (Entry> extensionConfigSectionsEntry : mergedModel.getGeneratedConfigSections() - .entrySet()) { - Extension extension = extensionConfigSectionsEntry.getKey(); - - for (ConfigSection generatedConfigSection : extensionConfigSectionsEntry.getValue()) { - if (generatedConfigSection.getNonDeprecatedItems().isEmpty()) { - continue; - } - - Path configSectionAdocPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, - extension.artifactId(), cleanSectionPath(generatedConfigSection.getPath().property()))); - String summaryTableId = asciidocFormatter - .toAnchor(extension.artifactId() + "_" + generatedConfigSection.getPath().property()); - - try { - Files.writeString(configSectionAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, - "_" + generatedConfigSection.getPath().property(), false)); - } catch (Exception e) { - throw new MojoExecutionException( - "Unable to render config section for section: " + generatedConfigSection.getPath().property() - + " in extension: " + extension, - e); - } - } - } - - if (generateAllConfig) { - // we generate the file centralizing all the config properties - try { - Path allConfigAdocPath = resolvedTargetDirectory.resolve(ALL_CONFIG_FILE_NAME); - - Files.writeString(allConfigAdocPath, generateAllConfig(quteEngine, mergedModel.getConfigRoots())); - } catch (Exception e) { - throw new MojoExecutionException("Unable to render all config", e); - } - } - } - - private static String generateConfigReference(Engine quteEngine, String summaryTableId, Extension extension, - ConfigItemCollection configItemCollection, String additionalAnchorPrefix, boolean searchable) { - return quteEngine.getTemplate("configReference.qute.adoc") - .data("extension", extension) - .data("configItemCollection", configItemCollection) - .data("searchable", searchable) - .data("summaryTableId", summaryTableId) - .data("additionalAnchorPrefix", additionalAnchorPrefix) - .data("includeDurationNote", configItemCollection.hasDurationType()) - .data("includeMemorySizeNote", configItemCollection.hasMemorySizeType()) - .render(); - } - - private static String generateAllConfig(Engine quteEngine, - Map> configRootsByExtensions) { - return quteEngine.getTemplate("allConfig.qute.adoc") - .data("configRootsByExtensions", configRootsByExtensions) - .data("searchable", true) - .data("summaryTableId", "all-config") - .data("additionalAnchorPrefix", "") - .data("includeDurationNote", true) - .data("includeMemorySizeNote", true) - .render(); - } - - private static void initTargetDirectory(Path resolvedTargetDirectory) throws MojoExecutionException { - try { - Files.createDirectories(resolvedTargetDirectory); - } catch (IOException e) { - throw new MojoExecutionException("Unable to create directory: " + resolvedTargetDirectory, e); - } - } - - private static List findTargetDirectories(Path scanDirectory) throws MojoExecutionException { - try { - List targets = new ArrayList<>(); - - Files.walkFileTree(scanDirectory, new SimpleFileVisitor<>() { - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (dir.endsWith(TARGET)) { - targets.add(dir); - - // a target directory can contain target directories for test projects - // so let's make sure we ignore whatever is nested in a target - return FileVisitResult.SKIP_SUBTREE; - } - - return FileVisitResult.CONTINUE; - } - }); - - // Make sure we are deterministic - Collections.sort(targets); - - return targets; - } catch (IOException e) { - throw new MojoExecutionException("Unable to collect the target directories", e); - } - } - - private static Engine initializeQuteEngine(AsciidocFormatter asciidocFormatter) { - Engine engine = Engine.builder() - .addDefaults() - .addSectionHelper(new UserTagSectionHelper.Factory("configProperty", "configProperty.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("configSection", "configSection.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("envVar", "envVar.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("durationNote", "durationNote.qute.adoc")) - .addSectionHelper(new UserTagSectionHelper.Factory("memorySizeNote", "memorySizeNote.qute.adoc")) - .addValueResolver(new ReflectionValueResolver()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(String.class) - .applyToName("escapeCellContent") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.escapeCellContent((String) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(String.class) - .applyToName("toAnchor") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.toAnchor((String) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigRootKey.class) - .applyToName("displayConfigRootDescription") - .applyToParameters(1) - .resolveSync(ctx -> asciidocFormatter - .displayConfigRootDescription((ConfigRootKey) ctx.getBase(), - (int) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("toAnchor") - .applyToParameters(2) - .resolveSync(ctx -> asciidocFormatter - .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) - .artifactId() + - // the additional suffix - ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + - "_" + ((ConfigProperty) ctx.getBase()).getPath().property())) - .build()) - // we need a different anchor for sections as otherwise we can have a conflict - // (typically when you have an `enabled` property with parent name just under the section level) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("toAnchor") - .applyToParameters(2) - .resolveSync(ctx -> asciidocFormatter - .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) - .artifactId() + - // the additional suffix - ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + - "_section_" + ((ConfigSection) ctx.getBase()).getPath().property())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatTypeDescription") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatTypeDescription((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatDescription") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatDescription((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigProperty.class) - .applyToName("formatDefaultValue") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatDefaultValue((ConfigProperty) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("formatTitle") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatSectionTitle((ConfigSection) ctx.getBase())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(ConfigSection.class) - .applyToName("adjustedLevel") - .applyToParameters(1) - .resolveSync(ctx -> asciidocFormatter - .adjustedLevel((ConfigSection) ctx.getBase(), - (boolean) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) - .build()) - .addValueResolver(ValueResolver.builder() - .applyToBaseClass(Extension.class) - .applyToName("formatName") - .applyToNoParameters() - .resolveSync(ctx -> asciidocFormatter.formatName((Extension) ctx.getBase())) - .build()) - .build(); - - engine.putTemplate("configReference.qute.adoc", - engine.parse(getTemplate("templates/configReference.qute.adoc"))); - engine.putTemplate("allConfig.qute.adoc", - engine.parse(getTemplate("templates/allConfig.qute.adoc"))); - engine.putTemplate("configProperty.qute.adoc", - engine.parse(getTemplate("templates/tags/configProperty.qute.adoc"))); - engine.putTemplate("configSection.qute.adoc", - engine.parse(getTemplate("templates/tags/configSection.qute.adoc"))); - engine.putTemplate("envVar.qute.adoc", - engine.parse(getTemplate("templates/tags/envVar.qute.adoc"))); - engine.putTemplate("durationNote.qute.adoc", - engine.parse(getTemplate("templates/tags/durationNote.qute.adoc"))); - engine.putTemplate("memorySizeNote.qute.adoc", - engine.parse(getTemplate("templates/tags/memorySizeNote.qute.adoc"))); - - return engine; - } - - private static String getTemplate(String template) { - InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(template); - if (is == null) { - throw new IllegalArgumentException("Template does not exist: " + template); - } - - try { - return new String(is.readAllBytes(), StandardCharsets.UTF_8); - } catch (IOException e) { - throw new UncheckedIOException("Unable to read the template: " + template, e); - } finally { - try { - is.close(); - } catch (IOException e) { - throw new UncheckedIOException("Unable close InputStream for template: " + template, e); - } - } - } - - /** - * A section path can contain quotes when being inside a Map. - *

- * While not very common, we sometimes have to generate a section inside a Map - * e.g. for the XDS config of the gRPC client. - */ - private static String cleanSectionPath(String sectionPath) { - return sectionPath.replace('"', '-'); - } - - private String stripAdocSuffix(String fileName) { - return fileName.substring(0, fileName.length() - ADOC_SUFFIX.length()); - } +public class GenerateAsciidocMojo extends GenerateConfigDocMojo { } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java new file mode 100644 index 0000000000000..ed5626b9f1bf1 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java @@ -0,0 +1,433 @@ +package io.quarkus.maven.config.doc; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocMerger; +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; +import io.quarkus.annotation.processor.documentation.config.merger.ModelMerger; +import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.maven.config.doc.generator.Format; +import io.quarkus.maven.config.doc.generator.Formatter; +import io.quarkus.qute.Engine; +import io.quarkus.qute.ReflectionValueResolver; +import io.quarkus.qute.UserTagSectionHelper; +import io.quarkus.qute.ValueResolver; + +@Mojo(name = "generate-config-doc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) +public class GenerateConfigDocMojo extends AbstractMojo { + + private static final String TARGET = "target"; + + private static final String ADOC_SUFFIX = ".adoc"; + private static final String CONFIG_ROOT_FILE_FORMAT = "%s_%s.%s"; + private static final String EXTENSION_FILE_FORMAT = "%s.%s"; + private static final String ALL_CONFIG_FILE_FORMAT = "quarkus-all-config.%s"; + + @Parameter(defaultValue = "${session}", readonly = true) + private MavenSession mavenSession; + + // if this is switched to something else at some point, GenerateAsciidocMojo will need to be adapted + @Parameter(defaultValue = "asciidoc") + private String format; + + @Parameter(defaultValue = Format.DEFAULT_THEME) + private String theme; + + @Parameter + private File scanDirectory; + + @Parameter(defaultValue = "${project.build.directory}/quarkus-generated-doc/config", required = true) + private File targetDirectory; + + @Parameter(defaultValue = "false") + private boolean generateAllConfig; + + @Parameter(defaultValue = "false") + private boolean enableEnumTooltips; + + @Parameter(defaultValue = "false") + private boolean skip; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + return; + } + + // I was unable to find an easy way to get the root directory of the project + Path resolvedScanDirectory = scanDirectory != null ? scanDirectory.toPath() + : mavenSession.getCurrentProject().getBasedir().toPath().getParent(); + Path resolvedTargetDirectory = targetDirectory.toPath(); + initTargetDirectory(resolvedTargetDirectory); + + List targetDirectories = findTargetDirectories(resolvedScanDirectory); + + JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); + MergedModel mergedModel = ModelMerger.mergeModel(javadocRepository, targetDirectories); + + Format normalizedFormat = Format.normalizeFormat(format); + + String normalizedTheme = normalizedFormat.normalizeTheme(theme); + Formatter formatter = Formatter.getFormatter(javadocRepository, enableEnumTooltips, normalizedFormat); + Engine quteEngine = initializeQuteEngine(formatter, normalizedFormat, normalizedTheme); + + // we generate a file per extension + top level prefix + for (Entry> extensionConfigRootsEntry : mergedModel.getConfigRoots() + .entrySet()) { + Extension extension = extensionConfigRootsEntry.getKey(); + + Path configRootPath = null; + + for (Entry configRootEntry : extensionConfigRootsEntry.getValue().entrySet()) { + String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); + ConfigRoot configRoot = configRootEntry.getValue(); + + // here we generate a file even if there are no items as it's used for the Reactive Oracle SQL client + + configRootPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, + extension.artifactId(), topLevelPrefix, normalizedFormat.getExtension())); + String summaryTableId = formatter + .toAnchor(extension.artifactId() + "_" + topLevelPrefix); + + try { + Files.writeString(configRootPath, + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render config roots for top level prefix: " + topLevelPrefix + + " in extension: " + extension, e); + } + } + + // if we have only one top level prefix, we copy the generated file to a file named after the extension + // for simplicity's sake + if (extensionConfigRootsEntry.getValue().size() == 1 && configRootPath != null) { + Path extensionPath = resolvedTargetDirectory.resolve(String.format(EXTENSION_FILE_FORMAT, + extension.artifactId(), normalizedFormat.getExtension())); + + try { + Files.copy(configRootPath, extensionPath, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new MojoExecutionException("Unable to copy extension file for: " + extension, e); + } + } + } + + // we generate the config roots that are saved in a specific file + for (Entry specificFileConfigRootEntry : mergedModel.getConfigRootsInSpecificFile().entrySet()) { + String annotationFileName = specificFileConfigRootEntry.getKey(); + ConfigRoot configRoot = specificFileConfigRootEntry.getValue(); + Extension extension = configRoot.getExtension(); + + if (configRoot.getNonDeprecatedItems().isEmpty()) { + continue; + } + + String normalizedFileName = stripAdocSuffix(annotationFileName); + String fileName = normalizedFileName + "." + normalizedFormat.getExtension(); + + Path configRootPath = resolvedTargetDirectory.resolve(fileName); + String summaryTableId = formatter.toAnchor(normalizedFileName); + + try { + Files.writeString(configRootPath, + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render config roots for specific file: " + fileName + + " in extension: " + extension, e); + } + } + + // we generate files for generated sections + for (Entry> extensionConfigSectionsEntry : mergedModel.getGeneratedConfigSections() + .entrySet()) { + Extension extension = extensionConfigSectionsEntry.getKey(); + + for (ConfigSection generatedConfigSection : extensionConfigSectionsEntry.getValue()) { + if (generatedConfigSection.getNonDeprecatedItems().isEmpty()) { + continue; + } + + Path configSectionPath = resolvedTargetDirectory.resolve(String.format(CONFIG_ROOT_FILE_FORMAT, + extension.artifactId(), cleanSectionPath(generatedConfigSection.getPath().property()), + normalizedFormat.getExtension())); + String summaryTableId = formatter + .toAnchor(extension.artifactId() + "_" + generatedConfigSection.getPath().property()); + + try { + Files.writeString(configSectionPath, + generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, + "_" + generatedConfigSection.getPath().property(), false)); + } catch (Exception e) { + throw new MojoExecutionException( + "Unable to render config section for section: " + generatedConfigSection.getPath().property() + + " in extension: " + extension, + e); + } + } + } + + if (generateAllConfig) { + // we generate the file centralizing all the config properties + try { + Path allConfigPath = resolvedTargetDirectory.resolve(String.format(ALL_CONFIG_FILE_FORMAT, + normalizedFormat.getExtension())); + + Files.writeString(allConfigPath, generateAllConfig(quteEngine, mergedModel.getConfigRoots())); + } catch (Exception e) { + throw new MojoExecutionException("Unable to render all config", e); + } + } + } + + private static String generateConfigReference(Engine quteEngine, String summaryTableId, Extension extension, + ConfigItemCollection configItemCollection, String additionalAnchorPrefix, boolean searchable) { + return quteEngine.getTemplate("configReference") + .data("extension", extension) + .data("configItemCollection", configItemCollection) + .data("searchable", searchable) + .data("summaryTableId", summaryTableId) + .data("additionalAnchorPrefix", additionalAnchorPrefix) + .data("includeDurationNote", configItemCollection.hasDurationType()) + .data("includeMemorySizeNote", configItemCollection.hasMemorySizeType()) + .render(); + } + + private static String generateAllConfig(Engine quteEngine, + Map> configRootsByExtensions) { + return quteEngine.getTemplate("allConfig") + .data("configRootsByExtensions", configRootsByExtensions) + .data("searchable", true) + .data("summaryTableId", "all-config") + .data("additionalAnchorPrefix", "") + .data("includeDurationNote", true) + .data("includeMemorySizeNote", true) + .render(); + } + + private static void initTargetDirectory(Path resolvedTargetDirectory) throws MojoExecutionException { + try { + Files.createDirectories(resolvedTargetDirectory); + } catch (IOException e) { + throw new MojoExecutionException("Unable to create directory: " + resolvedTargetDirectory, e); + } + } + + private static List findTargetDirectories(Path scanDirectory) throws MojoExecutionException { + try { + List targets = new ArrayList<>(); + + Files.walkFileTree(scanDirectory, new SimpleFileVisitor<>() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (dir.endsWith(TARGET)) { + targets.add(dir); + + // a target directory can contain target directories for test projects + // so let's make sure we ignore whatever is nested in a target + return FileVisitResult.SKIP_SUBTREE; + } + + return FileVisitResult.CONTINUE; + } + }); + + // Make sure we are deterministic + Collections.sort(targets); + + return targets; + } catch (IOException e) { + throw new MojoExecutionException("Unable to collect the target directories", e); + } + } + + private static Engine initializeQuteEngine(Formatter formatter, Format format, String theme) { + Engine engine = Engine.builder() + .addDefaults() + .addSectionHelper(new UserTagSectionHelper.Factory("configProperty", "configProperty")) + .addSectionHelper(new UserTagSectionHelper.Factory("configSection", "configSection")) + .addSectionHelper(new UserTagSectionHelper.Factory("envVar", "envVar")) + .addSectionHelper(new UserTagSectionHelper.Factory("durationNote", "durationNote")) + .addSectionHelper(new UserTagSectionHelper.Factory("memorySizeNote", "memorySizeNote")) + .addValueResolver(new ReflectionValueResolver()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(String.class) + .applyToName("escapeCellContent") + .applyToNoParameters() + .resolveSync(ctx -> formatter.escapeCellContent((String) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(String.class) + .applyToName("toAnchor") + .applyToNoParameters() + .resolveSync(ctx -> formatter.toAnchor((String) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigRootKey.class) + .applyToName("displayConfigRootDescription") + .applyToParameters(1) + .resolveSync(ctx -> formatter + .displayConfigRootDescription((ConfigRootKey) ctx.getBase(), + (int) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("toAnchor") + .applyToParameters(2) + .resolveSync(ctx -> formatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_" + ((ConfigProperty) ctx.getBase()).getPath().property())) + .build()) + // we need a different anchor for sections as otherwise we can have a conflict + // (typically when you have an `enabled` property with parent name just under the section level) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("toAnchor") + .applyToParameters(2) + .resolveSync(ctx -> formatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_section_" + ((ConfigSection) ctx.getBase()).getPath().property())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatTypeDescription") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatTypeDescription((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatDescription") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatDescription((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatDefaultValue") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatDefaultValue((ConfigProperty) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("formatTitle") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatSectionTitle((ConfigSection) ctx.getBase())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigSection.class) + .applyToName("adjustedLevel") + .applyToParameters(1) + .resolveSync(ctx -> formatter + .adjustedLevel((ConfigSection) ctx.getBase(), + (boolean) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join())) + .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(Extension.class) + .applyToName("formatName") + .applyToNoParameters() + .resolveSync(ctx -> formatter.formatName((Extension) ctx.getBase())) + .build()) + .build(); + + engine.putTemplate("configReference", + engine.parse(getTemplate("templates", format, theme, "configReference", false))); + engine.putTemplate("allConfig", + engine.parse(getTemplate("templates", format, theme, "allConfig", false))); + engine.putTemplate("configProperty", + engine.parse(getTemplate("templates", format, theme, "configProperty", true))); + engine.putTemplate("configSection", + engine.parse(getTemplate("templates", format, theme, "configSection", true))); + engine.putTemplate("envVar", + engine.parse(getTemplate("templates", format, theme, "envVar", true))); + engine.putTemplate("durationNote", + engine.parse(getTemplate("templates", format, theme, "durationNote", true))); + engine.putTemplate("memorySizeNote", + engine.parse(getTemplate("templates", format, theme, "memorySizeNote", true))); + + return engine; + } + + private static String getTemplate(String root, Format format, String theme, String template, boolean tag) { + List candidates = new ArrayList<>(); + candidates.add( + root + "/" + format + "/" + theme + "/" + (tag ? "tags/" : "") + template + ".qute." + format.getExtension()); + if (!Format.DEFAULT_THEME.equals(theme)) { + candidates + .add(root + "/" + format + "/" + Format.DEFAULT_THEME + "/" + (tag ? "tags/" : "") + template + ".qute." + + format.getExtension()); + } + + InputStream is = null; + ; + for (String candidate : candidates) { + is = Thread.currentThread().getContextClassLoader().getResourceAsStream(candidate); + if (is != null) { + break; + } + } + + if (is == null) { + throw new IllegalArgumentException("Unable to find a template for these candidates " + candidates); + } + + try { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Unable to read the template: " + template, e); + } finally { + try { + is.close(); + } catch (IOException e) { + throw new UncheckedIOException("Unable to close InputStream for template: " + template, e); + } + } + } + + /** + * A section path can contain quotes when being inside a Map. + *

+ * While not very common, we sometimes have to generate a section inside a Map + * e.g. for the XDS config of the gRPC client. + */ + private static String cleanSectionPath(String sectionPath) { + return sectionPath.replace('"', '-'); + } + + private String stripAdocSuffix(String fileName) { + return fileName.substring(0, fileName.length() - ADOC_SUFFIX.length()); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java similarity index 75% rename from devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java rename to devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java index 1617471b3260b..49384bb924e52 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java @@ -1,32 +1,32 @@ -package io.quarkus.maven.config.doc; +package io.quarkus.maven.config.doc.generator; import java.text.Normalizer; import java.time.Duration; import java.util.Optional; import java.util.stream.Collectors; +import io.quarkus.annotation.processor.documentation.config.formatter.JavadocTransformer; import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; import io.quarkus.annotation.processor.documentation.config.model.Extension; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; import io.quarkus.annotation.processor.documentation.config.util.Types; -final class AsciidocFormatter { +abstract class AbstractFormatter implements Formatter { - private static final String TOOLTIP_MACRO = "tooltip:%s[%s]"; - private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "link:#%s[icon:question-circle[title=More information about the %s format]]"; + protected final JavadocRepository javadocRepository; + protected final boolean enableEnumTooltips; - private final JavadocRepository javadocRepository; - private final boolean enableEnumTooltips; - - AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + AbstractFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { this.javadocRepository = javadocRepository; this.enableEnumTooltips = enableEnumTooltips; } - boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { + @Override + public boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { if (mapSize <= 1) { return false; } @@ -34,7 +34,8 @@ boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize) { return configRootKey.description() != null; } - String formatDescription(ConfigProperty configProperty) { + @Override + public String formatDescription(ConfigProperty configProperty) { Optional javadocElement = javadocRepository.getElement(configProperty.getSourceClass(), configProperty.getSourceName()); @@ -42,7 +43,8 @@ String formatDescription(ConfigProperty configProperty) { return null; } - String description = javadocElement.get().description(); + String description = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), + javadocFormat()); if (description == null || description.isBlank()) { return null; } @@ -50,7 +52,8 @@ String formatDescription(ConfigProperty configProperty) { return description + "\n\n"; } - String formatTypeDescription(ConfigProperty configProperty) { + @Override + public String formatTypeDescription(ConfigProperty configProperty) { String typeContent = ""; if (configProperty.isEnum() && enableEnumTooltips) { @@ -62,14 +65,14 @@ String formatTypeDescription(ConfigProperty configProperty) { return "`" + e.getValue().configValue() + "`"; } - return String.format(TOOLTIP_MACRO, e.getValue().configValue(), - cleanTooltipContent(javadocElement.get().description())); + return tooltip(e.getValue().configValue(), JavadocTransformer + .transform(javadocElement.get().description(), javadocElement.get().format(), javadocFormat())); }) .collect(Collectors.joining(", ")); } else { typeContent = configProperty.getTypeDescription(); if (configProperty.getJavadocSiteLink() != null) { - typeContent = String.format("link:%s[%s]", configProperty.getJavadocSiteLink(), typeContent); + typeContent = link(configProperty.getJavadocSiteLink(), typeContent); } } if (configProperty.isList()) { @@ -77,17 +80,16 @@ String formatTypeDescription(ConfigProperty configProperty) { } if (Duration.class.getName().equals(configProperty.getType())) { - typeContent += " " + String.format(MORE_INFO_ABOUT_TYPE_FORMAT, - "duration-note-anchor-{summaryTableId}", Duration.class.getSimpleName()); + typeContent += " " + moreInformationAboutType("duration-note-anchor", Duration.class.getSimpleName()); } else if (Types.MEMORY_SIZE_TYPE.equals(configProperty.getType())) { - typeContent += " " + String.format(MORE_INFO_ABOUT_TYPE_FORMAT, - "memory-size-note-anchor-{summaryTableId}", "MemorySize"); + typeContent += " " + moreInformationAboutType("memory-size-note-anchor", "MemorySize"); } return typeContent; } - String formatDefaultValue(ConfigProperty configProperty) { + @Override + public String formatDefaultValue(ConfigProperty configProperty) { String defaultValue = configProperty.getDefaultValue(); if (defaultValue == null) { @@ -105,7 +107,7 @@ String formatDefaultValue(ConfigProperty configProperty) { enumConstant.get()); if (javadocElement.isPresent()) { - return String.format(TOOLTIP_MACRO, defaultValue, cleanTooltipContent(javadocElement.get().description())); + return tooltip(defaultValue, javadocElement.get().description()); } } } @@ -113,7 +115,8 @@ String formatDefaultValue(ConfigProperty configProperty) { return "`" + defaultValue + "`"; } - int adjustedLevel(ConfigSection configSection, boolean multiRoot) { + @Override + public int adjustedLevel(ConfigSection configSection, boolean multiRoot) { if (multiRoot) { return configSection.getLevel() + 1; } @@ -121,7 +124,8 @@ int adjustedLevel(ConfigSection configSection, boolean multiRoot) { return configSection.getLevel(); } - String escapeCellContent(String value) { + @Override + public String escapeCellContent(String value) { if (value == null) { return null; } @@ -129,7 +133,8 @@ String escapeCellContent(String value) { return value.replace("|", "\\|"); } - String toAnchor(String value) { + @Override + public String toAnchor(String value) { // remove accents value = Normalizer.normalize(value, Normalizer.Form.NFKC) .replaceAll("[àáâãäåāąă]", "a") @@ -185,7 +190,8 @@ String toAnchor(String value) { return value.toLowerCase(); } - String formatSectionTitle(ConfigSection configSection) { + @Override + public String formatSectionTitle(ConfigSection configSection) { Optional javadocElement = javadocRepository.getElement(configSection.getSourceClass(), configSection.getSourceName()); @@ -194,7 +200,8 @@ String formatSectionTitle(ConfigSection configSection) { "Couldn't find section title for: " + configSection.getSourceClass() + "#" + configSection.getSourceName()); } - String javadoc = javadocElement.get().description(); + String javadoc = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), + javadocFormat()); if (javadoc == null || javadoc.isBlank()) { throw new IllegalStateException( "Couldn't find section title for: " + configSection.getSourceClass() + "#" + configSection.getSourceName()); @@ -203,7 +210,8 @@ String formatSectionTitle(ConfigSection configSection) { return trimFinalDot(javadoc); } - String formatName(Extension extension) { + @Override + public String formatName(Extension extension) { if (extension.name() == null) { return extension.artifactId(); } @@ -226,14 +234,11 @@ private static String trimFinalDot(String javadoc) { return javadoc.substring(0, dotIndex); } - /** - * Note that this is extremely brittle. Apparently, colons breaks the tooltips but if escaped with \, the \ appears in the - * output. - *

- * We should probably have some warnings/errors as to what is accepted in enum Javadoc. - */ - private String cleanTooltipContent(String tooltipContent) { - return tooltipContent.replace("

", "").replace("

", "").replace("\n+\n", " ").replace("\n", " ") - .replace(":", "\\:").replace("[", "\\]").replace("]", "\\]"); - } + protected abstract JavadocFormat javadocFormat(); + + protected abstract String moreInformationAboutType(String anchorRoot, String type); + + protected abstract String link(String href, String description); + + protected abstract String tooltip(String value, String javadocDescription); } diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java new file mode 100644 index 0000000000000..dab5e3d1aa1d3 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java @@ -0,0 +1,42 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +final class AsciidocFormatter extends AbstractFormatter { + + private static final String TOOLTIP_MACRO = "tooltip:%s[%s]"; + private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "link:#%s[icon:question-circle[title=More information about the %s format]]"; + + AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(javadocRepository, enableEnumTooltips); + } + + @Override + protected JavadocFormat javadocFormat() { + return JavadocFormat.ASCIIDOC; + } + + protected String moreInformationAboutType(String anchorRoot, String type) { + return String.format(MORE_INFO_ABOUT_TYPE_FORMAT, anchorRoot + "-{summaryTableId}", type); + } + + protected String tooltip(String value, String javadocDescription) { + return String.format(TOOLTIP_MACRO, value, cleanTooltipContent(javadocDescription)); + } + + /** + * Note that this is extremely brittle. Apparently, colons breaks the tooltips but if escaped with \, the \ appears in the + * output. + *

+ * We should probably have some warnings/errors as to what is accepted in enum Javadoc. + */ + private String cleanTooltipContent(String tooltipContent) { + return tooltipContent.replace("

", "").replace("

", "").replace("\n+\n", " ").replace("\n", " ") + .replace(":", "\\:").replace("[", "\\]").replace("]", "\\]"); + } + + protected String link(String href, String description) { + return String.format("link:%s[%s]", href, description); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java new file mode 100644 index 0000000000000..905d8fa14b74e --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java @@ -0,0 +1,35 @@ +package io.quarkus.maven.config.doc.generator; + +import java.util.Set; + +public enum Format { + + asciidoc("adoc", Set.of(Format.DEFAULT_THEME)), + markdown("md", Set.of(Format.DEFAULT_THEME, "github")); + + public static final String DEFAULT_THEME = "default"; + + private final String extension; + private final Set supportedThemes; + + private Format(String extension, Set supportedThemes) { + this.extension = extension; + this.supportedThemes = supportedThemes; + } + + public String getExtension() { + return extension; + } + + public String normalizeTheme(String theme) { + theme = theme.trim(); + + return supportedThemes.contains(theme) ? theme : DEFAULT_THEME; + } + + public static Format normalizeFormat(String format) { + format = format.trim(); + + return Format.valueOf(format); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java new file mode 100644 index 0000000000000..3552ab26f6e20 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java @@ -0,0 +1,39 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel.ConfigRootKey; +import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; + +public interface Formatter { + + boolean displayConfigRootDescription(ConfigRootKey configRootKey, int mapSize); + + String formatDescription(ConfigProperty configProperty); + + String formatTypeDescription(ConfigProperty configProperty); + + String formatDefaultValue(ConfigProperty configProperty); + + int adjustedLevel(ConfigSection configSection, boolean multiRoot); + + String escapeCellContent(String value); + + String toAnchor(String value); + + String formatSectionTitle(ConfigSection configSection); + + String formatName(Extension extension); + + static Formatter getFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips, Format format) { + switch (format) { + case asciidoc: + return new AsciidocFormatter(javadocRepository, enableEnumTooltips); + case markdown: + return new MarkdownFormatter(javadocRepository, enableEnumTooltips); + default: + throw new IllegalArgumentException("Unsupported format: " + format); + } + } +} \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java new file mode 100644 index 0000000000000..425e7481134fe --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java @@ -0,0 +1,48 @@ +package io.quarkus.maven.config.doc.generator; + +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; + +final class MarkdownFormatter extends AbstractFormatter { + + private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "[🛈](#%s)"; + + MarkdownFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(javadocRepository, enableEnumTooltips); + } + + @Override + protected JavadocFormat javadocFormat() { + return JavadocFormat.MARKDOWN; + } + + @Override + public String formatSectionTitle(ConfigSection configSection) { + // markdown only has 6 heading levels + int headingLevel = Math.min(6, 2 + configSection.getLevel()); + return "#".repeat(headingLevel) + " " + super.formatSectionTitle(configSection); + } + + @Override + public String escapeCellContent(String value) { + String cellContent = super.escapeCellContent(value); + return cellContent == null ? null : cellContent.replace("\n\n", "

").replace("\n", " "); + } + + @Override + protected String moreInformationAboutType(String anchorRoot, String type) { + return MORE_INFO_ABOUT_TYPE_FORMAT.formatted(anchorRoot); + } + + @Override + protected String link(String href, String description) { + return String.format("[%2$s](%1$s)", href, description); + } + + @Override + protected String tooltip(String value, String javadocDescription) { + // we don't have tooltip support in Markdown + return "`" + value + "`"; + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml b/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml index 615f650ef1fa4..4d890ae972d01 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml +++ b/devtools/config-doc-maven-plugin/src/main/resources/META-INF/plexus/components.xml @@ -11,7 +11,7 @@ - io.quarkus:quarkus-config-doc-maven-plugin:${project.version}:generate-asciidoc + io.quarkus:quarkus-config-doc-maven-plugin:${project.version}:generate-config-doc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/allConfig.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/allConfig.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc similarity index 94% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc index c694aeaf7a289..c5151982992c2 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc @@ -16,6 +16,9 @@ h|Default {#configProperty configProperty=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {/if} +{#else} +3+|No configuration properties found. + {/for} |=== diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configProperty.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configProperty.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configSection.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/configSection.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/durationNote.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/durationNote.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/durationNote.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/durationNote.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/envVar.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/envVar.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/envVar.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/envVar.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/memorySizeNote.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/memorySizeNote.qute.adoc similarity index 100% rename from devtools/config-doc-maven-plugin/src/main/resources/templates/tags/memorySizeNote.qute.adoc rename to devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/tags/memorySizeNote.qute.adoc diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md new file mode 100644 index 0000000000000..5b89612f0fa23 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md @@ -0,0 +1,27 @@ +🔒: Configuration property fixed at build time - All other configuration properties are overridable at runtime + +{#for extensionConfigRootsEntry in configRootsByExtensions} + +# {extensionConfigRootsEntry.key.formatName} + +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for configRoot in extensionConfigRootsEntry.value.values} +{#for item in configRoot.items} +{#if !item.deprecated} +{#if !item.isSection} +{#configProperty configProperty=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} +{#else} +{#configSection configSection=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} +{/if} +{/if} +{/for} +{/for} +{/for} + +{#if includeDurationNote} +{#durationNote summaryTableId /} +{/if} +{#if includeMemorySizeNote} +{#memorySizeNote summaryTableId /} +{/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md new file mode 100644 index 0000000000000..3c5237e397e66 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md @@ -0,0 +1,20 @@ +🔒: Configuration property fixed at build time - All other configuration properties are overridable at runtime + +{#if configItemCollection.nonDeprecatedProperties} +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for property in configItemCollection.nonDeprecatedProperties} +{#configProperty configProperty=property extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} +{/if} +{#for section in configItemCollection.nonDeprecatedSections} + +{#configSection configSection=section extension=extension additionalAnchorPrefix=additionalAnchorPrefix displayConfigRootDescription=false /} +{/for} + +{#if includeDurationNote} +{#durationNote summaryTableId /} +{/if} +{#if includeMemorySizeNote} +{#memorySizeNote summaryTableId /} +{/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md new file mode 100644 index 0000000000000..ce0462b7b2ce1 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md @@ -0,0 +1 @@ +| {#if configProperty.phase.fixedAtBuildTime}🔒 {/if}`{configProperty.path.property}`{#for additionalPath in configProperty.additionalPaths}
`{additionalPath.property}`{/for}

{configProperty.formatDescription.escapeCellContent.or("")}{#envVar configProperty /} | {configProperty.formatTypeDescription.escapeCellContent.or("")} | {#if configProperty.defaultValue}{configProperty.formatDefaultValue.escapeCellContent}{/if} | diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md new file mode 100644 index 0000000000000..69f5a2012af94 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md @@ -0,0 +1,13 @@ +{configSection.formatTitle} + +{#if configSection.nonDeprecatedProperties} +| Configuration property | Type | Default | +|------------------------|------|---------| +{#for property in configSection.nonDeprecatedProperties} +{#configProperty configProperty=property extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} +{/if} +{#for subsection in configSection.nonDeprecatedSections} + +{#configSection configSection=subsection extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} +{/for} \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md new file mode 100644 index 0000000000000..94d2ce93f735d --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md @@ -0,0 +1,17 @@ + + +> [!NOTE] +> ### About the Duration format +> +> To write duration values, use the standard `java.time.Duration` format. +> See the [Duration#parse()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)) Java API documentation] for more information. +> +> You can also use a simplified format, starting with a number: +> +> * If the value is only a number, it represents time in seconds. +> * If the value is a number followed by `ms`, it represents time in milliseconds. +> +> In other cases, the simplified format is translated to the `java.time.Duration` format for parsing: +> +> * If the value is a number followed by `h`, `m`, or `s`, it is prefixed with `PT`. +> * If the value is a number followed by `d`, it is prefixed with `P`. diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md new file mode 100644 index 0000000000000..0037f6eee6545 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md @@ -0,0 +1 @@ +Environment variable: `{configProperty.path.environmentVariable}` \ No newline at end of file diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md new file mode 100644 index 0000000000000..f6ce0be9db3ff --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md @@ -0,0 +1,8 @@ + + +> [!NOTE] +> ### About the MemorySize format +> +> A size configuration option recognizes strings in this format (shown as a regular expression): `[0-9]+[KkMmGgTtPpEeZzYy]?`. +> +> If no suffix is given, assume bytes. diff --git a/docs/pom.xml b/docs/pom.xml index ee82231a6665a..fe8a3b2c4e450 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -3210,7 +3210,7 @@ generate-config-doc process-resources - generate-asciidoc + generate-config-doc ${skipDocs} diff --git a/docs/src/main/asciidoc/amqp-reference.adoc b/docs/src/main/asciidoc/amqp-reference.adoc index e5bcdce9ecf22..74cc19c2daa6b 100644 --- a/docs/src/main/asciidoc/amqp-reference.adoc +++ b/docs/src/main/asciidoc/amqp-reference.adoc @@ -478,6 +478,19 @@ public AmqpClientOptions getNamedOptions() { } ---- +== TLS Configuration + +AMQP 1.0 Messaging extension integrates with the xref:./tls-registry-reference.adoc[Quarkus TLS registry] to configure the Vert.x AMQP client. + +To configure the TLS for an AMQP 1.0 channel, you need to provide a named TLS configuration in the `application.properties`: + +[source, properties] +---- +quarkus.tls.your-tls-config.trust-store.pem.certs=ca.crt,ca2.pem +# ... +mp.messaging.incoming.prices.tls-configuration-name=your-tls-config +---- + == Health reporting If you use the AMQP connector with the `quarkus-smallrye-health` extension, it contributes to the readiness and liveness probes. diff --git a/docs/src/main/asciidoc/datasource.adoc b/docs/src/main/asciidoc/datasource.adoc index 4828026f53e22..70cfc46a743fa 100644 --- a/docs/src/main/asciidoc/datasource.adoc +++ b/docs/src/main/asciidoc/datasource.adoc @@ -952,7 +952,7 @@ include::{generated-dir}/config/quarkus-reactive-mssql-client.adoc[opts=optional ==== Reactive Oracle-specific configuration -At the moment, there are no Oracle-specific configuration properties. +include::{generated-dir}/config/quarkus-reactive-oracle-client.adoc[opts=optional, leveloffset=+1] ==== Reactive PostgreSQL-specific configuration diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index 0abd8ab7adf6d..52bfa5b39d77d 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -2153,6 +2153,36 @@ Update the `oauth.client.id`, `oauth.client.secret` and `oauth.token.endpoint.ur OAuth authentication works for both JVM and native modes. Since SSL in not enabled by default in native mode, `quarkus.ssl.native=true` must be added to support JaasClientOauthLoginCallbackHandler, which uses SSL. (See the xref:native-and-ssl.adoc[Using SSL with Native Executables] guide for more details.) +== TLS Configuration + +Kafka client extension integrates with the xref:./tls-registry-reference.adoc[Quarkus TLS registry] to configure clients. + +To configure the TLS for the default Kafka configuration, you need to provide a named TLS configuration in the `application.properties`: + +[source, properties] +---- +quarkus.tls.your-tls-config.trust-store.pem.certs=target/certs/kafka.crt,target/certs/kafka-ca.crt +# ... +kafka.tls-configuration-name=your-tls-config +# enable ssl security protocol +kafka.security.protocol=ssl +---- + +This will in turn provide the Kafka client with a `ssl.engine.factory.class` implementation. + +[IMPORTANT] +==== +Make sure also to enable the SSL channel security protocol using the `security.protocol` property configured to `SSL` or `SASL_SSL`. +==== + +Quarkus Messaging channels can be configured individually to use a specific TLS configuration: + +[source, properties] +---- +mp.messaging.incoming.your-channel.tls-configuration-name=your-tls-config +mp.messaging.incoming.your-channel.security.protocol=ssl +---- + == Testing a Kafka application === Testing without a broker diff --git a/docs/src/main/asciidoc/messaging.adoc b/docs/src/main/asciidoc/messaging.adoc index 05724027b4a0d..38bd19ddd68c6 100644 --- a/docs/src/main/asciidoc/messaging.adoc +++ b/docs/src/main/asciidoc/messaging.adoc @@ -600,6 +600,33 @@ You can disable tracing for a specific channel using the following configuration mp.messaging.incoming.data.tracing-enabled=false ---- +== TLS Configuration + +Some messaging extensions integrate with the xref:./tls-registry-reference.adoc[Quarkus TLS Registry] to configure the underlying client. +To configure the TLS on a channel, you need to provide the named TLS configuration to the `tls-configuration-name` property: + +[source, properties] +---- +quarkus.tls.my-tls-config.trust-store=truststore.jks +quarkus.tls.my-tls-config.trust-store-password=secret +mp.messaging.incoming.my-channel.tls-configuration-name=my-tls-config +---- + +Or you can configure it globally on all channels of a connector: + +[source, properties] +---- +mp.messaging.connector.smallrye-pulsar.tls-configuration-name=my-tls-config +---- + +Currently, the following messaging extensions support configuration through the Quarkus TLS Registry: + +* Kafka: Provides the `ssl.engine.factory.class` property for the Kafka client. +* Pulsar: Only mTLS authentication is supported. +* RabbitMQ +* AMQP 1.0 +* MQTT + == Testing === Testing with Dev Services diff --git a/docs/src/main/asciidoc/pulsar.adoc b/docs/src/main/asciidoc/pulsar.adoc index fa61691f869cc..bf2203449a9a4 100644 --- a/docs/src/main/asciidoc/pulsar.adoc +++ b/docs/src/main/asciidoc/pulsar.adoc @@ -1118,6 +1118,23 @@ class PulsarConfig { } ---- +==== Configuring authentication to Pulsar using mTLS + +Pulsar Messaging extension integrates with the xref:./tls-registry-reference.adoc[Quarkus TLS registry] to authenticate clients using mTLS. + +To configure the mTLS for a Pulsar channel, you need to provide a named TLS configuration in the `application.properties`: + +[source, properties] +---- +quarkus.tls.my-tls-config.trust-store.p12.path=target/certs/pulsar-client-truststore.p12 +quarkus.tls.my-tls-config.trust-store.p12.password=secret +quarkus.tls.my-tls-config.key-store.p12.path=target/certs/pulsar-client-keystore.p12 +quarkus.tls.my-tls-config.key-store.p12.password=secret + +mp.messaging.incoming.prices.tls-configuration-name=my-tls-config +---- + + ==== Configuring access to Datastax Luna Streaming Luna Streaming is a production-ready distribution of Apache Pulsar, with tools and support from DataStax. diff --git a/docs/src/main/asciidoc/rabbitmq-reference.adoc b/docs/src/main/asciidoc/rabbitmq-reference.adoc index a4bf77788ea9e..5b2922ddb55be 100644 --- a/docs/src/main/asciidoc/rabbitmq-reference.adoc +++ b/docs/src/main/asciidoc/rabbitmq-reference.adoc @@ -389,6 +389,19 @@ You need to indicate the name of the client using the `client-options-name` attr mp.messaging.incoming.prices.client-options-name=my-named-options ---- +== TLS Configuration + +RabbitMQ Messaging extension integrates with the xref:./tls-registry-reference.adoc[Quarkus TLS registry] to configure the Vert.x RabbitMQ client. + +To configure the TLS for a channel, you need to provide a named TLS configuration in the `application.properties`: + +[source, properties] +---- +quarkus.tls.your-tls-config.trust-store.pem.certs=ca.crt,ca2.pem +# ... +mp.messaging.incoming.prices.tls-configuration-name=your-tls-config +---- + == Health reporting If you use the RabbitMQ connector with the `quarkus-smallrye-health` extension, it contributes to the readiness and liveness probes. diff --git a/docs/src/main/asciidoc/reactive-sql-clients.adoc b/docs/src/main/asciidoc/reactive-sql-clients.adoc index 4ce05554a1b89..5b64977d6a91c 100644 --- a/docs/src/main/asciidoc/reactive-sql-clients.adoc +++ b/docs/src/main/asciidoc/reactive-sql-clients.adoc @@ -922,7 +922,7 @@ include::{generated-dir}/config/quarkus-reactive-mssql-client.adoc[opts=optional === Oracle -At the moment, there are no Oracle-specific configuration properties. +include::{generated-dir}/config/quarkus-reactive-oracle-client.adoc[opts=optional, leveloffset=+1] === PostgreSQL diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index f6853f9f6c163..6e1969a7caf00 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -335,7 +335,7 @@ public boolean test(BeanInfo bean) { builder.setTransformPrivateInjectedFields(arcConfig.transformPrivateInjectedFields); builder.setFailOnInterceptedPrivateMethod(arcConfig.failOnInterceptedPrivateMethod); builder.setJtaCapabilities(capabilities.isPresent(Capability.TRANSACTIONS)); - builder.setGenerateSources(BootstrapDebug.DEBUG_SOURCES_DIR != null); + builder.setGenerateSources(BootstrapDebug.debugSourcesDir() != null); builder.setAllowMocking(launchModeBuildItem.getLaunchMode() == LaunchMode.TEST); builder.setStrictCompatibility(arcConfig.strictCompatibility); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java index b38f206af8951..6adbd0d320685 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java @@ -16,7 +16,7 @@ public class GeneratedBeanGizmoAdaptor implements ClassOutput { public GeneratedBeanGizmoAdaptor(BuildProducer classOutput) { this.classOutput = classOutput; - this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; } @Override diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessInheritanceTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessInheritanceTest.java index 1fff2080e1951..48b07f9779c35 100644 --- a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessInheritanceTest.java +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/PublicFieldAccessInheritanceTest.java @@ -2,6 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.logging.LogManager; + import jakarta.inject.Inject; import jakarta.persistence.Entity; import jakarta.persistence.EntityManager; @@ -16,6 +21,7 @@ import jakarta.transaction.UserTransaction; import org.hibernate.Hibernate; +import org.jboss.logmanager.Level; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -27,6 +33,19 @@ */ public class PublicFieldAccessInheritanceTest { + // FIXME Temporary debug options for https://github.com/quarkusio/quarkus/issues/42479 + // Needs to be set very early (e.g. as system properties) in order to affect the build; + // see https://quarkusio.zulipchat.com/#narrow/stream/187038-dev/topic/Build.20logs + private static final Map DEBUG_PROPERTIES = Map.of( + "quarkus.debug.transformed-classes-dir", "target/debug/${testRunId}/transformed-classes", + "quarkus.debug.generated-classes-dir", "target/debug/${testRunId}/generated-classes"); + // FIXME Temporary trace categories for https://github.com/quarkusio/quarkus/issues/42479 + // Needs to be set very early (e.g. programmatically) in order to affect the build; + // needs to be set programmatically in order to not leak to other tests. + // See https://quarkusio.zulipchat.com/#narrow/stream/187038-dev/topic/Build.20logs + // See https://github.com/quarkusio/quarkus/issues/43180 + private static final List TRACE_CATEGORIES = List.of("org.hibernate", "io.quarkus.hibernate", "io.quarkus.panache"); + @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar @@ -34,7 +53,31 @@ public class PublicFieldAccessInheritanceTest { .addClass(MyAbstractEntity.class) .addClass(MyConcreteEntity.class) .addClass(FieldAccessEnhancedDelegate.class)) - .withConfigurationResource("application.properties"); + .withConfigurationResource("application.properties") + // FIXME Temporary debug options for https://github.com/quarkusio/quarkus/issues/42479 + .overrideConfigKey("quarkus.hibernate-orm.log.sql", "true") + // Not doing this because it has side effects on other tests for some reason; + // see https://github.com/quarkusio/quarkus/issues/43180 + // It's not necessary anyway as the only effect of this config property is to change + // the logging level for a specific "org.hibernate.something" category, which we already do below. + //.overrideConfigKey("quarkus.hibernate-orm.log.bind-parameters", "true") + .setBeforeAllCustomizer(() -> { + // Used to differentiate reruns of flaky tests in Maven + var testRunId = PublicFieldAccessInheritanceTest.class + "/" + UUID.randomUUID(); + System.out.println("Test run ID: " + testRunId); + DEBUG_PROPERTIES.forEach((key, value) -> System.setProperty(key, value.replace("${testRunId}", testRunId))); + for (String category : TRACE_CATEGORIES) { + LogManager.getLogManager().getLogger(category) + .setLevel(Level.TRACE); + } + }) + .setAfterAllCustomizer(() -> { + DEBUG_PROPERTIES.keySet().forEach(System::clearProperty); + for (String category : TRACE_CATEGORIES) { + LogManager.getLogManager().getLogger(category) + .setLevel(null); + } + }); @Inject EntityManager em; diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 6fed92c085e9b..47b5d1b6d1e6c 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -69,6 +69,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; @@ -105,6 +106,7 @@ import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer; import io.quarkus.kafka.client.serialization.ObjectMapperSerializer; import io.quarkus.kafka.client.tls.QuarkusKafkaSslEngineFactory; +import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; public class KafkaProcessor { @@ -214,6 +216,7 @@ void relaxSaslElytron(BuildProducer config @BuildStep public void build( KafkaBuildTimeConfig config, CurateOutcomeBuildItem curateOutcomeBuildItem, + BuildProducer configDescBuildItems, CombinedIndexBuildItem indexBuildItem, BuildProducer reflectiveClass, BuildProducer serviceProviders, BuildProducer proxies, @@ -289,6 +292,8 @@ public void build( reflectiveClass.produce( ReflectiveClassBuildItem.builder(QuarkusKafkaSslEngineFactory.class).build()); + configDescBuildItems.produce(new ConfigDescriptionBuildItem("kafka.tls-configuration-name", null, + "The tls-configuration to use for the Kafka client", null, null, ConfigPhase.RUN_TIME)); } @BuildStep(onlyIf = { HasSnappy.class, NativeOrNativeSourcesBuild.class }) diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/dns/MongoDnsClient.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/dns/MongoDnsClient.java index d20705bedaec5..56ab2a4ddd155 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/dns/MongoDnsClient.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/dns/MongoDnsClient.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,6 +27,7 @@ import io.quarkus.mongodb.runtime.MongodbConfig; import io.quarkus.runtime.annotations.RegisterForReflection; +import io.smallrye.mutiny.Uni; import io.vertx.core.dns.DnsClientOptions; import io.vertx.mutiny.core.Vertx; import io.vertx.mutiny.core.dns.SrvRecord; @@ -130,12 +132,21 @@ private List resolveSrvRequest(final String srvHost) { if (SRV_CACHE.containsKey(srvHost)) { srvRecords = SRV_CACHE.get(srvHost); } else { - srvRecords = dnsClient.resolveSRV(srvHost).invoke(new Consumer<>() { - @Override - public void accept(List srvRecords) { - SRV_CACHE.put(srvHost, srvRecords); - } - }).await().atMost(timeout); + srvRecords = Uni.createFrom().> deferred( + new Supplier<>() { + @Override + public Uni> get() { + return dnsClient.resolveSRV(srvHost); + } + }) + .onFailure().retry().withBackOff(Duration.ofSeconds(1)).atMost(3) + .invoke(new Consumer<>() { + @Override + public void accept(List srvRecords) { + SRV_CACHE.put(srvHost, srvRecords); + } + }) + .await().atMost(timeout); } if (srvRecords.isEmpty()) { @@ -167,12 +178,22 @@ public List resolveTxtRequest(final String host) { try { Duration timeout = config.getOptionalValue(DNS_LOOKUP_TIMEOUT, Duration.class) .orElse(Duration.ofSeconds(5)); - return dnsClient.resolveTXT(host).invoke(new Consumer<>() { - @Override - public void accept(List strings) { - TXT_CACHE.put(host, strings); - } - }).await().atMost(timeout); + + return Uni.createFrom().> deferred( + new Supplier<>() { + @Override + public Uni> get() { + return dnsClient.resolveTXT(host); + } + }) + .onFailure().retry().withBackOff(Duration.ofSeconds(1)).atMost(3) + .invoke(new Consumer<>() { + @Override + public void accept(List strings) { + TXT_CACHE.put(host, strings); + } + }) + .await().atMost(timeout); } catch (Throwable e) { throw new MongoConfigurationException("Unable to look up TXT record for host " + host, e); } diff --git a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java index fd546210e454f..778e30be8ac13 100644 --- a/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java +++ b/extensions/reactive-oracle-client/runtime/src/main/java/io/quarkus/reactive/oracle/client/runtime/DataSourceReactiveOracleConfig.java @@ -5,5 +5,4 @@ @ConfigGroup public interface DataSourceReactiveOracleConfig { - // when adding properties here, make sure you include the generated doc in datasource.adoc and reactive-sql-clients.adoc } diff --git a/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java b/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java index ab833016c9bc7..a8d8eec468b01 100644 --- a/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-jaxb/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/runtime/serialisers/ServerJaxbMessageBodyReader.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.PushbackInputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; @@ -20,7 +21,6 @@ import jakarta.xml.bind.UnmarshalException; import jakarta.xml.bind.Unmarshaller; -import org.jboss.resteasy.reactive.common.util.StreamUtil; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -93,14 +93,16 @@ private Unmarshaller getUnmarshall(Class type) throws JAXBException { } private Object doReadFrom(Class type, Type genericType, InputStream entityStream) throws IOException { - if (isInputStreamEmpty(entityStream)) { + PushbackInputStream pushbackEntityStream = new PushbackInputStream(entityStream); + if (isStreamEmpty(pushbackEntityStream)) { return null; } - - return unmarshal(entityStream, type); + return unmarshal(pushbackEntityStream, type); } - private boolean isInputStreamEmpty(InputStream entityStream) throws IOException { - return StreamUtil.isEmpty(entityStream) || entityStream.available() == 0; + private boolean isStreamEmpty(PushbackInputStream pushbackStream) throws IOException { + int firstByte = pushbackStream.read(); + pushbackStream.unread(firstByte); + return firstByte == -1; } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 3f67e713f24bf..c7154a5b83acd 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -93,6 +93,7 @@ import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoTagFilter; +import io.quarkus.smallrye.openapi.deployment.filter.ClassAndMethod; import io.quarkus.smallrye.openapi.deployment.filter.SecurityConfigFilter; import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem; import io.quarkus.smallrye.openapi.deployment.spi.IgnoreStaticDocumentBuildItem; @@ -578,7 +579,8 @@ private OASFilter getAutoTagFilter(OpenApiFilteredIndexViewBuildItem apiFiltered SmallRyeOpenApiConfig config) { if (config.autoAddTags) { - Map classNamesMethodReferences = getClassNamesMethodReferences(apiFilteredIndexViewBuildItem); + Map classNamesMethodReferences = getClassNamesMethodReferences( + apiFilteredIndexViewBuildItem); if (!classNamesMethodReferences.isEmpty()) { return new AutoTagFilter(classNamesMethodReferences); @@ -680,7 +682,8 @@ private static Stream> getMethods(Anno return Stream.empty(); } - private Map getClassNamesMethodReferences(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { + private Map getClassNamesMethodReferences( + OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem) { FilteredIndexView filteredIndex = apiFilteredIndexViewBuildItem.getIndex(); List openapiAnnotations = new ArrayList<>(); Set allOpenAPIEndpoints = getAllOpenAPIEndpoints(); @@ -688,7 +691,7 @@ private Map getClassNamesMethodReferences(OpenApiFilteredIndexVi openapiAnnotations.addAll(filteredIndex.getAnnotations(dotName)); } - Map classNames = new HashMap<>(); + Map classNames = new HashMap<>(); for (AnnotationInstance ai : openapiAnnotations) { if (ai.target().kind().equals(AnnotationTarget.Kind.METHOD)) { @@ -704,7 +707,7 @@ private Map getClassNamesMethodReferences(OpenApiFilteredIndexVi .getAllKnownSubclasses(declaringClass.name()), classNames); } else { String ref = JandexUtil.createUniqueMethodReference(declaringClass, method); - classNames.put(ref, declaringClass.simpleName()); + classNames.put(ref, new ClassAndMethod(declaringClass.simpleName(), method.name())); } } } @@ -712,16 +715,18 @@ private Map getClassNamesMethodReferences(OpenApiFilteredIndexVi } void addMethodImplementationClassNames(MethodInfo method, Type[] params, Collection classes, - Map classNames) { + Map classNames) { for (ClassInfo impl : classes) { String simpleClassName = impl.simpleName(); MethodInfo implMethod = impl.method(method.name(), params); if (implMethod != null) { - classNames.put(JandexUtil.createUniqueMethodReference(impl, implMethod), simpleClassName); + classNames.put(JandexUtil.createUniqueMethodReference(impl, implMethod), + new ClassAndMethod(simpleClassName, implMethod.name())); } - classNames.put(JandexUtil.createUniqueMethodReference(impl, method), simpleClassName); + classNames.put(JandexUtil.createUniqueMethodReference(impl, method), + new ClassAndMethod(simpleClassName, method.name())); } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java index 0794d20aaff2d..22a119e1e6fe8 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java @@ -15,21 +15,21 @@ * Automatically tag operations based on the class name. */ public class AutoTagFilter implements OASFilter { - private Map classNameMap; + private Map classNameMap; public AutoTagFilter() { } - public AutoTagFilter(Map classNameMap) { + public AutoTagFilter(Map classNameMap) { this.classNameMap = classNameMap; } - public Map getClassNameMap() { + public Map getClassNameMap() { return classNameMap; } - public void setClassNameMap(Map classNameMap) { + public void setClassNameMap(Map classNameMap) { this.classNameMap = classNameMap; } @@ -45,12 +45,23 @@ public void filterOpenAPI(OpenAPI openAPI) { Map operations = pathItem.getValue().getOperations(); if (operations != null && !operations.isEmpty()) { for (Operation operation : operations.values()) { + + if (operation.getDescription() == null || operation.getDescription().isBlank()) { + // Auto add a description + OperationImpl operationImpl = (OperationImpl) operation; + String methodRef = operationImpl.getMethodRef(); + if (classNameMap.containsKey(methodRef)) { + operation.setDescription(capitalizeFirstLetter( + splitCamelCase(classNameMap.get(methodRef).methodName()))); + } + } + if (operation.getTags() == null || operation.getTags().isEmpty()) { // Auto add a tag OperationImpl operationImpl = (OperationImpl) operation; String methodRef = operationImpl.getMethodRef(); if (classNameMap.containsKey(methodRef)) { - operation.addTag(splitCamelCase(classNameMap.get(methodRef))); + operation.addTag(splitCamelCase(classNameMap.get(methodRef).className())); } } } @@ -69,4 +80,11 @@ private String splitCamelCase(String s) { "(?<=[A-Za-z])(?=[^A-Za-z])"), " "); } + + private String capitalizeFirstLetter(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } } \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/ClassAndMethod.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/ClassAndMethod.java new file mode 100644 index 0000000000000..d5f1bce941d38 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/ClassAndMethod.java @@ -0,0 +1,5 @@ +package io.quarkus.smallrye.openapi.deployment.filter; + +public record ClassAndMethod(String className, String methodName) { + +} diff --git a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java index 41fbd927a99a6..4edc06b5615a2 100644 --- a/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java +++ b/extensions/smallrye-reactive-messaging/deployment/src/main/java/io/quarkus/smallrye/reactivemessaging/deployment/WiringProcessor.java @@ -214,6 +214,9 @@ private void handleMethodAnnotationWithOutgoing(BuildProducer new DeploymentException("Empty @Outgoing annotation on method " + method))); } if (outgoing != null) { + configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( + "mp.messaging.outgoing." + outgoing.value().asString() + ".tls-configuration-name", null, + "The tls-configuration to use", null, null, ConfigPhase.RUN_TIME)); configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( "mp.messaging.outgoing." + outgoing.value().asString() + ".connector", null, "The connector to use", null, null, ConfigPhase.BUILD_TIME)); @@ -232,6 +235,9 @@ private void handleMethodAnnotationWithOutgoings(BuildProducer validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( new DeploymentException("Empty @Outgoing annotation on method " + method))); } + configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( + "mp.messaging.outgoing." + instance.value().asString() + ".tls-configuration-name", null, + "The tls-configuration to use", null, null, ConfigPhase.RUN_TIME)); configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( "mp.messaging.outgoing." + instance.value().asString() + ".connector", null, "The connector to use", null, null, ConfigPhase.BUILD_TIME)); @@ -250,6 +256,9 @@ private void handleMethodAnnotationWithIncomings(BuildProducer validationErrors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem( new DeploymentException("Empty @Incoming annotation on method " + method))); } + configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( + "mp.messaging.incoming." + instance.value().asString() + ".tls-configuration-name", null, + "The tls-configuration to use", null, null, ConfigPhase.RUN_TIME)); configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( "mp.messaging.incoming." + instance.value().asString() + ".connector", null, "The connector to use", null, null, ConfigPhase.BUILD_TIME)); @@ -267,6 +276,9 @@ private void handleMethodAnnotatedWithIncoming(BuildProducer a new DeploymentException("Empty @Incoming annotation on method " + method))); } if (incoming != null) { + configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( + "mp.messaging.incoming." + incoming.value().asString() + ".tls-configuration-name", null, + "The tls-configuration to use", null, null, ConfigPhase.RUN_TIME)); configDescriptionBuildItemBuildProducer.produce(new ConfigDescriptionBuildItem( "mp.messaging.incoming." + incoming.value().asString() + ".connector", null, "The connector to use", null, null, ConfigPhase.BUILD_TIME)); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js index 7c82175e701c4..e255967f68e0e 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -80,7 +80,8 @@ export class QwcContinuousTesting extends QwcHotReloadElement { _state: {state: true}, _results: {state: true}, _busy: {state: true}, - _detailsOpenedItem: {state: true, type: Array} + _detailsOpenedItem: {state: true, type: Array}, + _displayTags: {state: true, type: Boolean}, }; constructor() { @@ -89,6 +90,7 @@ export class QwcContinuousTesting extends QwcHotReloadElement { this._detailsOpenedItem = []; this._chartTitles = ["passed", "failed", "skipped"]; this._chartColors = ['--lumo-success-text-color', '--lumo-error-text-color', '--lumo-contrast-70pct']; + this._displayTags = true; } connectedCallback() { @@ -135,14 +137,56 @@ export class QwcContinuousTesting extends QwcHotReloadElement { } render() { + let results = this._prepareResultsToRender(); return html` - ${this._renderMenuBar()} - ${this._renderResultSet()} + ${this._renderMenuBar(results)} + ${this._renderResults(results)} ${this._renderBarChart()} `; } - _renderMenuBar(){ + _prepareResultsToRender() { + if(this._state && this._state.running && this._results && this._results.results) { + let itemsByState = { + passing: [], + failing: [], + skipped: [], + } + Object + .values(this._results.results) + .forEach(item => { + itemsByState.passing.push(...item.passing.filter( obj => obj.test === true )); + itemsByState.failing.push(...item.failing.filter( obj => obj.test === true )); + itemsByState.skipped.push(...item.skipped.filter( obj => obj.test === true )); + }); + let items = itemsByState.failing.concat( + itemsByState.passing, + itemsByState.skipped + ); + let hasTags = items.find( item => item.tags && item.tags.length > 0 ); + return { + items: items, + meta: { + hasTags: hasTags, + failing: itemsByState.failing.length, + passing: itemsByState.passing.length, + skipped: itemsByState.skipped.length, + }, + } + } else { + return { + items: [], + meta: { + hasTags: false, + failing: 0, + passing: 0, + skipped: 0, + }, + }; + } + } + + _renderMenuBar(results){ if(this._state){ return html``; @@ -193,62 +238,28 @@ export class QwcContinuousTesting extends QwcHotReloadElement { } - _renderResultSet(){ - if(this._state && this._state.running && this._results && this._results.results) { - - let failingResults = this._results.failing; - let passingResults = this._results.passing; - let skippedResults = this._results.skipped; - - var allResults = failingResults.concat(passingResults, skippedResults); - - return html`${this._renderResults(allResults)}`; - } - - } - _renderResults(results){ - if(results.length > 0){ + if(results.items.length > 0){ + + return html` + + ${ + this._displayTags && results.meta.hasTags + ? html` this._tagsRenderer(prop), [])}>` + : '' + } + this._testRenderer(prop), [])}> + this._nameRenderer(prop), [])}> + this._timeRenderer(prop), [])}>> + `; - let items = []; - - for (let i = 0; i < results.length; i++) { - let result = results[i]; - - let failingResult = result.failing.filter(function( obj ) { - return obj.test === true; - }); - let passingResult = result.passing.filter(function( obj ) { - return obj.test === true; - }); - let skippedResult = result.skipped.filter(function( obj ) { - return obj.test === true; - }); - - items.push.apply(items, failingResult); - items.push.apply(items, passingResult); - items.push.apply(items, skippedResult); - } - - if(items.length>0){ - return html` - - - this._testRenderer(prop), [])}> - this._nameRenderer(prop), [])}> - this._timeRenderer(prop), [])}>> - `; - }else{ - return html`No tests`; - } - }else{ return html`No tests`; } @@ -286,6 +297,35 @@ export class QwcContinuousTesting extends QwcHotReloadElement { )}`; } + _tagToColor(tag){ + // Step 0: two strings with the last char differing by 1 should render to totally different colors + const tagValue = tag + tag; + // Step 1: Convert the string to a numeric hash value + let hash = 0; + for (let i = 0; i < tagValue.length; i++) { + hash = tagValue.charCodeAt(i) + ((hash << 5) - hash); + } + + // Step 2: Convert the numeric hash value to a hex color code + let color = '#'; + const normalizeFactor = 0.2; // cut 20% light and dark values + for (let i = 0; i < 3; i++) { + const value = Math.round(((hash >> (i * 8)) & 0xFF) * (1-2*normalizeFactor) + 255*normalizeFactor); + color += ('00' + value.toString(16)).slice(-2); + } + + return color; + } + + _tagsRenderer(testLine){ + return html`${testLine.tags.map((tag, index) => { + const color = this._tagToColor(tag); + return html` + ${"io.quarkus.test.junit.QuarkusTest" === tag ? "Q" : tag} + `; + })}`; + } + _testRenderer(testLine){ let level = testLine.testExecutionResult.status.toLowerCase(); @@ -369,6 +409,17 @@ export class QwcContinuousTesting extends QwcHotReloadElement { } } + _renderToggleDisplayTags(results) { + if(this._state && this._state.running){ + return html` + `; + } + } + _start(){ if(!this._busy){ this._busy = true; @@ -407,5 +458,9 @@ export class QwcContinuousTesting extends QwcHotReloadElement { this._busy = false; }); } + + _toggleDisplayTags(){ + this._displayTags = !this._displayTags; + } } customElements.define('qwc-continuous-testing', QwcContinuousTesting); \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java index b9f28a85dee5a..d17b2ddcf9e8b 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -124,8 +124,9 @@ public String getHTMLContent() { // Additional Endpoints if (!this.additionalEndpoints.isEmpty()) { + List endpoints = getSortedAdditionalRouteDescriptions(); builder.resourcesStart(ADDITIONAL_ENDPOINTS); - for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + for (AdditionalRouteDescription additionalEndpoint : endpoints) { builder.staticResourcePath(additionalEndpoint.getUri(), additionalEndpoint.getDescription()); } builder.resourcesEnd(); @@ -189,7 +190,8 @@ public JsonObject getJsonContent() { // Additional Endpoints if (!this.additionalEndpoints.isEmpty()) { JsonArray ae = new JsonArray(); - for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + List endpoints = getSortedAdditionalRouteDescriptions(); + for (AdditionalRouteDescription additionalEndpoint : endpoints) { ae.add(JsonObject.of(URI, additionalEndpoint.getUri(), DESCRIPTION, additionalEndpoint.getDescription())); } infoMap.put(ADDITIONAL_ENDPOINTS, ae); @@ -250,7 +252,8 @@ public String getTextContent() { // Additional Endpoints if (!this.additionalEndpoints.isEmpty()) { sw.write(ADDITIONAL_ENDPOINTS + NL); - for (AdditionalRouteDescription additionalEndpoint : this.additionalEndpoints) { + List endpoints = getSortedAdditionalRouteDescriptions(); + for (AdditionalRouteDescription additionalEndpoint : endpoints) { sw.write(TAB + "- " + additionalEndpoint.getUri() + NL); sw.write(TAB + TAB + "- " + additionalEndpoint.getDescription() + NL); } @@ -341,6 +344,13 @@ private boolean isHtmlFileName(String fileName) { return fileName.endsWith(".html") || fileName.endsWith(".htm") || fileName.endsWith(".xhtml"); } + private List getSortedAdditionalRouteDescriptions() { + return this.additionalEndpoints.stream().sorted( + Comparator.comparingInt((AdditionalRouteDescription desc) -> desc.getUri().split("/").length) + .thenComparing(AdditionalRouteDescription::getUri)) + .toList(); + } + private static final String HEADING = "404 - Resource Not Found"; private static final String RESOURCE_ENDPOINTS = "Resource Endpoints"; private static final String SERVLET_MAPPINGS = "Servlet mappings"; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapDebug.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapDebug.java index 604d6c23838ba..befb358db400d 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapDebug.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/BootstrapDebug.java @@ -2,12 +2,39 @@ public final class BootstrapDebug { - public static final String DEBUG_SOURCES_DIR = System.getProperty("quarkus.debug.generated-sources-dir"); - + /** + * @deprecated Use {@link #debugClassesDir()} instead for more flexibility when testing. + */ + @Deprecated public static final String DEBUG_CLASSES_DIR = System.getProperty("quarkus.debug.generated-classes-dir"); + /** + * @deprecated Use {@link #transformedClassesDir()} instead for more flexibility when testing. + */ + @Deprecated public static final String DEBUG_TRANSFORMED_CLASSES_DIR = System.getProperty("quarkus.debug.transformed-classes-dir"); + /** + * @deprecated Use {@link #debugSourcesDir()} instead for more flexibility when testing. + */ + @Deprecated + public static final String DEBUG_SOURCES_DIR = System.getProperty("quarkus.debug.generated-sources-dir"); + + // We're exposing the configuration properties as methods and not constants, + // because in the case of tests, the system property could change over the life of the JVM. + + public static String debugClassesDir() { + return System.getProperty("quarkus.debug.generated-classes-dir"); + } + + public static String transformedClassesDir() { + return System.getProperty("quarkus.debug.transformed-classes-dir"); + } + + public static String debugSourcesDir() { + return System.getProperty("quarkus.debug.generated-sources-dir"); + } + private BootstrapDebug() { }