From 280013ea0fdecdc557d3421c9ee3aa054fbd2367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 6 Sep 2024 11:00:56 +0200 Subject: [PATCH 01/16] Expose BootstrapDebug properties as methods instead of constants So that tests can change them between executions -- in particular in QuarkusUnitTest, which re-builds the application for each test class. --- .../GeneratedClassGizmoAdaptor.java | 4 +-- .../steps/ClassTransformingBuildStep.java | 5 +-- .../runner/bootstrap/AugmentActionImpl.java | 2 +- .../runner/bootstrap/StartupActionImpl.java | 7 +++-- .../quarkus/arc/deployment/ArcProcessor.java | 2 +- .../deployment/GeneratedBeanGizmoAdaptor.java | 2 +- .../io/quarkus/bootstrap/BootstrapDebug.java | 31 +++++++++++++++++-- 7 files changed, 41 insertions(+), 12 deletions(-) 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/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/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/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() { } From e701c303ace24ceac63916b1c62db93cf5025ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 6 Sep 2024 09:32:38 +0200 Subject: [PATCH 02/16] Enable debug dumps and trace logging in PublicFieldAccessInheritanceTest To investigate transient failures, see https://github.com/quarkusio/quarkus/issues/42479 --- .../PublicFieldAccessInheritanceTest.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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; From 7b345819c8bc44d1bdbed443d62aeb59e91d9c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 6 Sep 2024 11:29:54 +0200 Subject: [PATCH 03/16] Upload test-produced debug dumps in GitHub Actions CI --- .github/workflows/ci-actions-incremental.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) 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() }} From 2d28d32301ba6b3bd463d276ff59b7c6f1bbd035 Mon Sep 17 00:00:00 2001 From: yamada-y0 Date: Mon, 9 Sep 2024 21:20:01 +0900 Subject: [PATCH 04/16] Sort the list of endpoints in the generated dev 404 --- .../runtime/devmode/ResourceNotFoundData.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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"; From a235d4aed7f08bb98105836476b026951eea70a1 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 30 Aug 2024 15:01:03 +0200 Subject: [PATCH 05/16] Allow multiple format and themes in the Config Doc generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idea is to allow generating Markdown for easily publishing the config on GitHub. The Javadoc -> Markdown part will probably change a lot soon as we need to support Markdown for the IDEs. This is the fruit of a collaboration with @Athou (Jérémie Panzer), who is marked as a co-author. Co-authored-by: Jérémie Panzer --- .../config/discovery/ParsedJavadoc.java | 5 +- .../JavadocToAsciidocTransformer.java | 57 ++- .../config/model/ConfigItemCollection.java | 14 + .../config/model/JavadocElements.java | 4 +- .../scanner/JavadocConfigMappingListener.java | 2 +- .../JavadocLegacyConfigRootListener.java | 2 +- .../config/doc/GenerateAsciidocMojo.java | 407 +--------------- .../config/doc/GenerateConfigDocMojo.java | 435 ++++++++++++++++++ .../AbstractFormatter.java} | 72 +-- .../doc/generator/AsciidocFormatter.java | 42 ++ .../maven/config/doc/generator/Format.java | 35 ++ .../maven/config/doc/generator/Formatter.java | 39 ++ .../doc/generator/MarkdownFormatter.java | 48 ++ .../resources/META-INF/plexus/components.xml | 2 +- .../default}/allConfig.qute.adoc | 0 .../default}/configReference.qute.adoc | 0 .../default}/tags/configProperty.qute.adoc | 0 .../default}/tags/configSection.qute.adoc | 0 .../default}/tags/durationNote.qute.adoc | 0 .../default}/tags/envVar.qute.adoc | 0 .../default}/tags/memorySizeNote.qute.adoc | 0 .../markdown/default/allConfig.qute.md | 27 ++ .../markdown/default/configReference.qute.md | 20 + .../default/tags/configProperty.qute.md | 1 + .../default/tags/configSection.qute.md | 13 + .../default/tags/durationNote.qute.md | 17 + .../markdown/default/tags/envVar.qute.md | 1 + .../default/tags/memorySizeNote.qute.md | 8 + docs/pom.xml | 2 +- 29 files changed, 807 insertions(+), 446 deletions(-) create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java rename devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/{AsciidocFormatter.java => generator/AbstractFormatter.java} (75%) create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AsciidocFormatter.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Format.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/Formatter.java create mode 100644 devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/MarkdownFormatter.java rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/allConfig.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/configReference.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/configProperty.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/configSection.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/durationNote.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/envVar.qute.adoc (100%) rename devtools/config-doc-maven-plugin/src/main/resources/templates/{ => asciidoc/default}/tags/memorySizeNote.qute.adoc (100%) create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/allConfig.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/configReference.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configProperty.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/configSection.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/durationNote.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/envVar.qute.md create mode 100644 devtools/config-doc-maven-plugin/src/main/resources/templates/markdown/default/tags/memorySizeNote.qute.md 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..7dda7f935a508 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,9 +1,10 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadoc(String description, String since, String deprecated, JavadocFormat originalFormat) { +public record ParsedJavadoc(String description, String since, String deprecated, + String originalDescription, JavadocFormat originalFormat) { public static ParsedJavadoc empty() { - return new ParsedJavadoc(null, null, null, null); + return new ParsedJavadoc(null, null, null, null, null); } public boolean isEmpty() { 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..2f8c05ad64ed1 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 @@ -95,14 +95,17 @@ public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { Javadoc javadoc = StaticJavaParser.parseJavadoc(START_OF_LINE.matcher(rawJavadoc).replaceAll("* ")); String description; + String originalDescription; JavadocFormat originalFormat; if (isAsciidoc(javadoc)) { description = handleEolInAsciidoc(javadoc); originalFormat = JavadocFormat.ASCIIDOC; + originalDescription = rawJavadoc; } else { description = htmlJavadocToAsciidoc(javadoc.getDescription()); originalFormat = JavadocFormat.JAVADOC; + originalDescription = filterRawJavadoc(javadoc.getDescription()); } Optional since = javadoc.getBlockTags().stream() @@ -121,7 +124,8 @@ public ParsedJavadoc parseConfigItemJavadoc(String rawJavadoc) { description = null; } - return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalFormat); + return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalDescription, + originalFormat); } public ParsedJavadocSection parseConfigSectionJavadoc(String javadocComment) { @@ -546,4 +550,55 @@ private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { } return sb; } + + /** + * 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 filterRawJavadoc(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/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..55aa09bbeb954 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, String since, String deprecated, String rawJavadoc) { } public boolean isEmpty() { 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..50a08674f411f 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 @@ -64,7 +64,7 @@ public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElem configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + parsedJavadoc.originalDescription())); } } } 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..be8c164f43e60 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 @@ -64,7 +64,7 @@ public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeEleme configCollector.addJavadocElement( clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), parsedJavadoc.deprecated(), - rawJavadoc.get())); + parsedJavadoc.originalDescription())); } } } 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..dee67e7ce959f 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 { +} \ No newline at end of file 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..0530062b329b2 --- /dev/null +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateConfigDocMojo.java @@ -0,0 +1,435 @@ +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(); + + if (configRoot.getNonDeprecatedItems().isEmpty()) { + continue; + } + + 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..81f37d5b13d2a 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,4 +1,4 @@ -package io.quarkus.maven.config.doc; +package io.quarkus.maven.config.doc.generator; import java.text.Normalizer; import java.time.Duration; @@ -13,20 +13,18 @@ import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; 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 +32,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 +41,7 @@ String formatDescription(ConfigProperty configProperty) { return null; } - String description = javadocElement.get().description(); + String description = javadoc(javadocElement.get()); if (description == null || description.isBlank()) { return null; } @@ -50,7 +49,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 +62,13 @@ 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(), javadoc(javadocElement.get())); }) .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 +76,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 +103,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 +111,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 +120,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 +129,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 +186,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()); @@ -203,7 +205,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 +229,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 String javadoc(JavadocElement javadocElement); + + 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..e0d834eb1c225 --- /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.JavadocElements; + +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); + } + + 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); + } + + @Override + protected String javadoc(JavadocElements.JavadocElement javadocElement) { + return javadocElement.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..eaf13fda73cf7 --- /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.JavadocElements; + +final class MarkdownFormatter extends AbstractFormatter { + + private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "[🛈](#%s)"; + + MarkdownFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { + super(javadocRepository, enableEnumTooltips); + } + + @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 + "`"; + } + + @Override + protected String javadoc(JavadocElements.JavadocElement javadocElement) { + return javadocElement.rawJavadoc(); + } +} 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 100% 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 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} From ec49d73af8577c8f1510bf393759b5bbce408189 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 17:29:54 +0200 Subject: [PATCH 06/16] Config Doc - Reinstate Reactive Oracle Client doc In the end, let's keep it around but handles it properly in the generated config doc. --- .../io/quarkus/maven/config/doc/GenerateAsciidocMojo.java | 2 +- .../io/quarkus/maven/config/doc/GenerateConfigDocMojo.java | 4 +--- .../templates/asciidoc/default/configReference.qute.adoc | 3 +++ docs/src/main/asciidoc/datasource.adoc | 2 +- docs/src/main/asciidoc/reactive-sql-clients.adoc | 2 +- .../oracle/client/runtime/DataSourceReactiveOracleConfig.java | 1 - 6 files changed, 7 insertions(+), 7 deletions(-) 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 dee67e7ce959f..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 @@ -10,4 +10,4 @@ */ @Mojo(name = "generate-asciidoc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) public class GenerateAsciidocMojo extends GenerateConfigDocMojo { -} \ No newline at end of file +} 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 index 0530062b329b2..ed5626b9f1bf1 100644 --- 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 @@ -111,9 +111,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { String topLevelPrefix = configRootEntry.getKey().topLevelPrefix(); ConfigRoot configRoot = configRootEntry.getValue(); - if (configRoot.getNonDeprecatedItems().isEmpty()) { - continue; - } + // 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())); diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/asciidoc/default/configReference.qute.adoc b/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/asciidoc/default/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/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/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/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 } From 3cfa644c4924cc6d2f0721fa6b16eede58949ae4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 19:33:04 +0200 Subject: [PATCH 07/16] Config Doc - Prepare the arrival of Markdown For IDEs, we will need to be able to convert our javadoc to Markdown. And obviously, transforming our javadoc to Asciidoc right in the generated YAML files is a bad idea. We now pass the raw Javadoc and it is going to be transformed at a later stage. --- .../config/ConfigDocExtensionProcessor.java | 7 +- .../config/discovery/JavadocFormat.java | 6 - .../config/discovery/ParsedJavadoc.java | 7 +- .../discovery/ParsedJavadocSection.java | 6 +- .../JavadocToAsciidocTransformer.java | 237 +++--------------- .../JavadocToMarkdownTransformer.java | 87 +++++++ .../config/formatter/JavadocTransformer.java | 20 ++ .../config/model/JavadocElements.java | 2 +- .../config/model/JavadocFormat.java | 8 + .../AbstractJavadocConfigListener.java | 22 +- .../scanner/JavadocConfigMappingListener.java | 39 +-- .../JavadocLegacyConfigRootListener.java | 39 +-- .../config/util/JavadocUtil.java | 158 ++++++++++++ .../processor/util/ElementUtil.java | 7 +- ...ocToAsciidocTransformerConfigItemTest.java | 199 ++++++++------- ...oAsciidocTransformerConfigSectionTest.java | 90 +------ .../config/util/JavadocUtilTest.java | 110 ++++++++ .../doc/generator/AbstractFormatter.java | 13 +- .../doc/generator/AsciidocFormatter.java | 12 +- .../doc/generator/MarkdownFormatter.java | 12 +- 20 files changed, 632 insertions(+), 449 deletions(-) delete mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/JavadocFormat.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToMarkdownTransformer.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocTransformer.java create mode 100644 core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocFormat.java 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 7dda7f935a508..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,10 +1,11 @@ package io.quarkus.annotation.processor.documentation.config.discovery; -public record ParsedJavadoc(String description, String since, String deprecated, - String originalDescription, 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, null); + return new ParsedJavadoc(null, null, null, null); } public boolean isEmpty() { 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/formatter/JavadocToAsciidocTransformer.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/formatter/JavadocToAsciidocTransformer.java index 2f8c05ad64ed1..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,130 +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; - String originalDescription; - JavadocFormat originalFormat; - - if (isAsciidoc(javadoc)) { - description = handleEolInAsciidoc(javadoc); - originalFormat = JavadocFormat.ASCIIDOC; - originalDescription = rawJavadoc; - } else { - description = htmlJavadocToAsciidoc(javadoc.getDescription()); - originalFormat = JavadocFormat.JAVADOC; - originalDescription = filterRawJavadoc(javadoc.getDescription()); - } - - 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(); + public static String toAsciidoc(String javadoc, JavadocFormat format) { + return toAsciidoc(javadoc, format, false); + } - if (description != null && description.isBlank()) { - description = null; + public static String toAsciidoc(String javadoc, JavadocFormat format, boolean inlineMacroMode) { + if (javadoc == null || javadoc.isBlank()) { + return null; } - return new ParsedJavadoc(description, since.orElse(null), deprecated.orElse(null), originalDescription, - 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(); @@ -208,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: @@ -217,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: @@ -225,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); @@ -258,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); @@ -267,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) @@ -275,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: @@ -335,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: @@ -356,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; } } @@ -435,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()) { @@ -501,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); @@ -550,55 +440,4 @@ private StringBuilder appendEscapedAsciiDoc(StringBuilder sb, String text) { } return sb; } - - /** - * 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 filterRawJavadoc(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/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/JavadocElements.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/JavadocElements.java index 55aa09bbeb954..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 @@ -4,7 +4,7 @@ public record JavadocElements(Extension extension, Map elements) { - public record JavadocElement(String description, String since, String deprecated, 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 50a08674f411f..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(), - parsedJavadoc.originalDescription())); + 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 be8c164f43e60..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(), - parsedJavadoc.originalDescription())); + 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/generator/AbstractFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java index 81f37d5b13d2a..49384bb924e52 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/generator/AbstractFormatter.java @@ -5,12 +5,14 @@ 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; abstract class AbstractFormatter implements Formatter { @@ -41,7 +43,8 @@ public String formatDescription(ConfigProperty configProperty) { return null; } - String description = javadoc(javadocElement.get()); + String description = JavadocTransformer.transform(javadocElement.get().description(), javadocElement.get().format(), + javadocFormat()); if (description == null || description.isBlank()) { return null; } @@ -62,7 +65,8 @@ public String formatTypeDescription(ConfigProperty configProperty) { return "`" + e.getValue().configValue() + "`"; } - return tooltip(e.getValue().configValue(), javadoc(javadocElement.get())); + return tooltip(e.getValue().configValue(), JavadocTransformer + .transform(javadocElement.get().description(), javadocElement.get().format(), javadocFormat())); }) .collect(Collectors.joining(", ")); } else { @@ -196,7 +200,8 @@ public 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()); @@ -229,7 +234,7 @@ private static String trimFinalDot(String javadoc) { return javadoc.substring(0, dotIndex); } - protected abstract String javadoc(JavadocElement javadocElement); + protected abstract JavadocFormat javadocFormat(); protected abstract String moreInformationAboutType(String anchorRoot, String type); 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 index e0d834eb1c225..dab5e3d1aa1d3 100644 --- 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 @@ -1,7 +1,7 @@ package io.quarkus.maven.config.doc.generator; import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; final class AsciidocFormatter extends AbstractFormatter { @@ -12,6 +12,11 @@ final class AsciidocFormatter extends AbstractFormatter { 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); } @@ -34,9 +39,4 @@ private String cleanTooltipContent(String tooltipContent) { protected String link(String href, String description) { return String.format("link:%s[%s]", href, description); } - - @Override - protected String javadoc(JavadocElements.JavadocElement javadocElement) { - return javadocElement.description(); - } } 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 index eaf13fda73cf7..425e7481134fe 100644 --- 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 @@ -2,7 +2,7 @@ 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.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocFormat; final class MarkdownFormatter extends AbstractFormatter { @@ -12,6 +12,11 @@ final class MarkdownFormatter extends AbstractFormatter { super(javadocRepository, enableEnumTooltips); } + @Override + protected JavadocFormat javadocFormat() { + return JavadocFormat.MARKDOWN; + } + @Override public String formatSectionTitle(ConfigSection configSection) { // markdown only has 6 heading levels @@ -40,9 +45,4 @@ protected String tooltip(String value, String javadocDescription) { // we don't have tooltip support in Markdown return "`" + value + "`"; } - - @Override - protected String javadoc(JavadocElements.JavadocElement javadocElement) { - return javadocElement.rawJavadoc(); - } } From 34c67b0d1cc6a138e9513988fc90b3e29069f661 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 22:33:17 +0200 Subject: [PATCH 08/16] Config Doc - Reset list status for passthrough maps --- .../processor/documentation/config/discovery/ResolvedType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From ea7d1c2767536082265de4f3e1e064633bc084b4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 10 Sep 2024 22:41:16 +0200 Subject: [PATCH 09/16] Update develocity-maven-extension to 1.22.1 --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e2becd4c49f76d47acb0b040ddf8cbb9e36546ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:28:25 +0000 Subject: [PATCH 10/16] Bump com.nimbusds:nimbus-jose-jwt from 9.40 to 9.41 Bumps [com.nimbusds:nimbus-jose-jwt](https://bitbucket.org/connect2id/nimbus-jose-jwt) from 9.40 to 9.41. - [Changelog](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/CHANGELOG.txt) - [Commits](https://bitbucket.org/connect2id/nimbus-jose-jwt/branches/compare/9.41..9.40) --- updated-dependencies: - dependency-name: com.nimbusds:nimbus-jose-jwt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 3827e33144f55c2f294e1d0e232a4602cc1e5f60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:35:59 +0000 Subject: [PATCH 11/16] Bump org.mvnpm.at.mvnpm:vaadin-webcomponents from 24.4.7 to 24.4.8 Bumps [org.mvnpm.at.mvnpm:vaadin-webcomponents](https://github.com/vaadin/web-components) from 24.4.7 to 24.4.8. - [Release notes](https://github.com/vaadin/web-components/releases) - [Commits](https://github.com/vaadin/web-components/compare/v24.4.7...v24.4.8) --- updated-dependencies: - dependency-name: org.mvnpm.at.mvnpm:vaadin-webcomponents dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- bom/dev-ui/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7205ccadbb0c90f92abc223300dd2e4e0dc4f32b Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Wed, 11 Sep 2024 13:52:01 +1000 Subject: [PATCH 12/16] Make sure OpenAPI AutoTag add an description if missing Signed-off-by: Phillip Kruger --- .../deployment/SmallRyeOpenApiProcessor.java | 19 ++++++++----- .../deployment/filter/AutoTagFilter.java | 28 +++++++++++++++---- .../deployment/filter/ClassAndMethod.java | 5 ++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/ClassAndMethod.java 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) { + +} From d17d9ca5767437edaaf960f31bb88f8577a3cba0 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Tue, 3 Sep 2024 08:25:48 +0200 Subject: [PATCH 13/16] Extend Continuous Testing page by tags. - introduce `tags` column - introduce toggle to hide the `tags`column - automatically hide `tags` column when JUnit tags are not used --- .../dev/testing/JunitTestRunner.java | 22 ++- .../deployment/dev/testing/TestResult.java | 9 +- .../dev-ui/qwc/qwc-continuous-testing.js | 171 ++++++++++++------ 3 files changed, 139 insertions(+), 63 deletions(-) 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/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 From 9368c13c4e23379943f354e13a304bc84a0e39a3 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Fri, 6 Sep 2024 11:29:12 +0200 Subject: [PATCH 14/16] Messaging extensions doc tls-registry usage update --- docs/src/main/asciidoc/amqp-reference.adoc | 13 ++++++++ docs/src/main/asciidoc/kafka.adoc | 30 +++++++++++++++++++ docs/src/main/asciidoc/messaging.adoc | 27 +++++++++++++++++ docs/src/main/asciidoc/pulsar.adoc | 17 +++++++++++ .../src/main/asciidoc/rabbitmq-reference.adoc | 13 ++++++++ .../client/deployment/KafkaProcessor.java | 5 ++++ .../deployment/WiringProcessor.java | 12 ++++++++ 7 files changed, 117 insertions(+) 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/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/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/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)); From f420b2542d033d6e3eb129df4f1d238c52c79a41 Mon Sep 17 00:00:00 2001 From: Andreas Stangl Date: Wed, 11 Sep 2024 10:38:34 +0200 Subject: [PATCH 15/16] More robust Stream emptiness check for ServerJaxbMessageBodyReader Previously, InputStream.available() was used to check the stream for emptiness. This can result in streams being mistakenly qualified as empty since available() only provides an estimate of how many bytes can be read without blocking. --- .../serialisers/ServerJaxbMessageBodyReader.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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; } } From 0b95574833a95cf179b2f3dc7399c803fbfc75b8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 11 Sep 2024 21:40:22 +0300 Subject: [PATCH 16/16] Introduce retry into Mongo DNS resolution --- .../mongodb/runtime/dns/MongoDnsClient.java | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) 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); }