diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index fcae87fee9..19b9bf3cd7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -21,6 +21,7 @@
hadoop = "3.4.1"
iceberg = "1.7.1"
dropwizard = "4.0.11"
+picocli = "4.7.6"
slf4j = "2.0.16"
swagger = "1.6.14"
@@ -40,6 +41,7 @@ bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version = "1
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.1.8" }
commons-codec1 = { module = "commons-codec:commons-codec", version = "1.17.2" }
commons-lang3 = { module = "org.apache.commons:commons-lang3", version = "3.17.0" }
+commons-text = { module = "org.apache.commons:commons-text", version = "1.13.0" }
dropwizard-bom = { module = "io.dropwizard:dropwizard-bom", version.ref = "dropwizard" }
eclipselink = { module = "org.eclipse.persistence:eclipselink", version = "4.0.5" }
errorprone = { module = "com.google.errorprone:error_prone_core", version = "2.36.0" }
@@ -67,10 +69,13 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version = "1.14.3" }
mockito-core = { module = "org.mockito:mockito-core", version = "5.15.2" }
opentelemetry-bom = { module = "io.opentelemetry:opentelemetry-bom", version = "1.46.0" }
opentelemetry-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version = "1.25.0-alpha" }
+picocli = { module = "info.picocli:picocli-codegen", version.ref = "picocli" }
+picocli-codegen = { module = "info.picocli:picocli-codegen", version.ref = "picocli" }
prometheus-metrics-exporter-servlet-jakarta = { module = "io.prometheus:prometheus-metrics-exporter-servlet-jakarta", version = "1.3.5" }
s3mock-testcontainers = { module = "com.adobe.testing:s3mock-testcontainers", version = "3.12.0" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
smallrye-common-annotation = { module = "io.smallrye.common:smallrye-common-annotation", version = "2.9.0" }
+smallrye-config-core = { module = "io.smallrye.config:smallrye-config-core", version = "3.10.2" }
spotbugs-annotations = { module = "com.github.spotbugs:spotbugs-annotations", version = "4.8.6" }
swagger-annotations = { module = "io.swagger:swagger-annotations", version.ref = "swagger" }
swagger-jaxrs = { module = "io.swagger:swagger-jaxrs", version.ref = "swagger" }
diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties
index 71490c3cb4..1938df25d3 100644
--- a/gradle/projects.main.properties
+++ b/gradle/projects.main.properties
@@ -29,3 +29,7 @@ polaris-jpa-model=extension/persistence/jpa-model
polaris-tests=integration-tests
aggregated-license-report=aggregated-license-report
polaris-version=tools/version
+
+polaris-config-docs-annotations=tools/config-docs/annotations
+polaris-config-docs-generator=tools/config-docs/generator
+polaris-config-docs-site=tools/config-docs/site
diff --git a/tools/config-docs/annotations/build.gradle.kts b/tools/config-docs/annotations/build.gradle.kts
new file mode 100644
index 0000000000..90bb2ac84d
--- /dev/null
+++ b/tools/config-docs/annotations/build.gradle.kts
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+plugins {
+ id("polaris-client")
+ `java-library`
+}
+
+description = "Polaris reference docs annotations"
diff --git a/tools/config-docs/annotations/src/main/java/org/apache/polaris/docs/ConfigDocs.java b/tools/config-docs/annotations/src/main/java/org/apache/polaris/docs/ConfigDocs.java
new file mode 100644
index 0000000000..6997f0b4fd
--- /dev/null
+++ b/tools/config-docs/annotations/src/main/java/org/apache/polaris/docs/ConfigDocs.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Contains annotations for {@code :polaris-config-doc-generator}. */
+public interface ConfigDocs {
+ /**
+ * For properties-configs, declares a class containing configuration constants for "properties"
+ * and gives it a page name. The generated markdown files will start with this name.
+ */
+ @Target(ElementType.TYPE)
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ConfigPageGroup {
+ String name();
+ }
+
+ /**
+ * Define the "section" in which the config option appears.
+ *
+ *
For properties-configs, this declares that a property constant field appears in the
+ * generated markdown files.
+ *
+ *
For smallrye-configs, this declares that a property appears under a different "prefix".
+ */
+ @Target({ElementType.FIELD, ElementType.METHOD})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ConfigItem {
+ /**
+ * The name of the "section" in which this constant field shall appear. The name of the
+ * generated markdown file will "end" with this name.
+ */
+ String section() default "";
+
+ /** For smallrye-configs only: the section docs are taken from property type's javadoc. */
+ boolean sectionDocFromType() default false;
+ }
+
+ /**
+ * For smallrye-configs, gives map-keys a name that appears as a placeholder in the fully
+ * qualified config name in the generated docs.
+ */
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ConfigPropertyName {
+ String value() default "";
+ }
+}
diff --git a/tools/config-docs/generator/build.gradle.kts b/tools/config-docs/generator/build.gradle.kts
new file mode 100644
index 0000000000..61ea0218fc
--- /dev/null
+++ b/tools/config-docs/generator/build.gradle.kts
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+plugins {
+ id("polaris-server")
+ `java-library`
+ `java-test-fixtures`
+}
+
+description = "Generates Polaris reference docs"
+
+val genTesting by configurations.creating
+
+dependencies {
+ implementation(project(":polaris-config-docs-annotations"))
+
+ implementation(libs.commons.text)
+
+ implementation(libs.smallrye.config.core)
+ implementation(libs.picocli)
+ annotationProcessor(libs.picocli.codegen)
+
+ testFixturesApi(platform(libs.junit.bom))
+ testFixturesApi("org.junit.jupiter:junit-jupiter")
+ testRuntimeOnly("org.junit.platform:junit-platform-launcher")
+ testFixturesApi(libs.assertj.core)
+ testFixturesApi(libs.mockito.core)
+
+ genTesting(project(":polaris-config-docs-annotations"))
+ genTesting(libs.smallrye.config.core)
+}
+
+tasks.named("test") {
+ // The test needs the classpath for the necessary dependencies (annotations + smallrye-config).
+ // Resolving the dependencies must happen during task execution (not configuration).
+ jvmArgumentProviders.add(
+ CommandLineArgumentProvider {
+ // So, in theory, all 'org.gradle.category' attributes should use the type
+ // org.gradle.api.attributes.Category,
+ // as Category.CATEGORY_ATTRIBUTE is defined. BUT! Some attributes have an attribute type ==
+ // String.class!
+
+ val categoryAttributeAsString = Attribute.of("org.gradle.category", String::class.java)
+
+ val libraries =
+ genTesting.incoming.artifacts
+ .filter { a ->
+ // dependencies:
+ // org.gradle.category=library
+ val category =
+ a.variant.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
+ ?: a.variant.attributes.getAttribute(categoryAttributeAsString)
+ category != null && category.toString() == Category.LIBRARY
+ }
+ .map { a -> a.file }
+
+ listOf("-Dtesting.libraries=" + libraries.joinToString(":"))
+ }
+ )
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/DocGenDoclet.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/DocGenDoclet.java
new file mode 100644
index 0000000000..f0c785e091
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/DocGenDoclet.java
@@ -0,0 +1,360 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static org.apache.polaris.docs.generator.SmallRyeConfigs.concatWithDot;
+
+import com.sun.source.doctree.DocCommentTree;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.lang.model.SourceVersion;
+import jdk.javadoc.doclet.Doclet;
+import jdk.javadoc.doclet.DocletEnvironment;
+import jdk.javadoc.doclet.Reporter;
+
+public class DocGenDoclet implements Doclet {
+
+ private Path outputDirectory = Paths.get(".");
+
+ private final Option directoryOption =
+ new Option() {
+ @Override
+ public int getArgumentCount() {
+ return 1;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Directory to write .md files to";
+ }
+
+ @Override
+ public Kind getKind() {
+ return Kind.STANDARD;
+ }
+
+ @Override
+ public List getNames() {
+ return List.of("-d", "--directory");
+ }
+
+ @Override
+ public String getParameters() {
+ return "directory";
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ outputDirectory = Paths.get(arguments.get(0));
+ return true;
+ }
+ };
+ private final Option notimestampDummy = new DummyOption(List.of("-notimestamp"), 0);
+ private final Option doctitleDummy = new DummyOption(List.of("-doctitle"), 1);
+ private final Option windowtitleDummy = new DummyOption(List.of("-windowtitle"), 1);
+
+ @Override
+ public boolean run(DocletEnvironment environment) {
+ var propertiesConfigs = new PropertiesConfigs(environment);
+ var smallryeConfigs = new SmallRyeConfigs(environment);
+
+ for (var includedElement : environment.getIncludedElements()) {
+ try {
+ includedElement.accept(propertiesConfigs.visitor(), null);
+ includedElement.accept(smallryeConfigs.visitor(), null);
+ } catch (RuntimeException ex) {
+ throw new RuntimeException("Failure processing included element " + includedElement, ex);
+ }
+ }
+
+ propertiesConfigPages(propertiesConfigs);
+
+ smallryeConfigPages(environment, smallryeConfigs);
+
+ return true;
+ }
+
+ private void propertiesConfigPages(PropertiesConfigs propertiesConfigs) {
+ for (var page : propertiesConfigs.pages()) {
+ System.out.println("Generating properties config pages for " + page.name());
+ for (Map.Entry> e : page.sectionItems().entrySet()) {
+ var section = e.getKey();
+ if (section.isEmpty()) {
+ section = "main";
+ }
+ System.out.println("... generating page section " + section);
+ var items = e.getValue();
+
+ var file = outputDirectory.resolve(page.name() + "-" + safeFileName(section) + ".md");
+ try (var fw = Files.newBufferedWriter(file, UTF_8, CREATE, TRUNCATE_EXISTING);
+ var writer = new PrintWriter(fw)) {
+ writer.println("| Property | Description |");
+ writer.println("|----------|-------------|");
+ for (var item : items) {
+ // TODO add _pluggable_ formatter (javadoc to markdown, later: javadoc to asciidoc?)
+ var md = new MarkdownPropertyFormatter(item);
+ if (!md.isHidden()) {
+ writer.print("| `");
+ writer.print(md.propertyName());
+ writer.print("` | ");
+ writer.print(md.description().replaceAll("\n", "
"));
+ writer.println(" |");
+ }
+ }
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+ }
+
+ private void smallryeConfigPages(DocletEnvironment environment, SmallRyeConfigs smallryeConfigs) {
+ var sectionPages = new HashMap();
+
+ for (var mappingInfo : smallryeConfigs.configMappingInfos()) {
+ smallryeProcessRootMappingInfo(environment, smallryeConfigs, mappingInfo, sectionPages);
+ }
+
+ sectionPages.values().stream()
+ .filter(p -> !p.isEmpty())
+ .forEach(
+ page -> {
+ System.out.printf(
+ "... generating smallrye config page for section %s%n", page.section);
+ var file = outputDirectory.resolve("smallrye-" + safeFileName(page.section) + ".md");
+ try (var pw =
+ new PrintWriter(
+ Files.newBufferedWriter(file, UTF_8, CREATE, TRUNCATE_EXISTING))) {
+ page.writeTo(pw);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ private void smallryeProcessRootMappingInfo(
+ DocletEnvironment environment,
+ SmallRyeConfigs smallryeConfigs,
+ SmallRyeConfigMappingInfo mappingInfo,
+ Map sectionPages) {
+ var effectiveSection = mappingInfo.prefix();
+ var propertyNamePrefix = mappingInfo.prefix();
+ smallryeProcessMappingInfo(
+ "",
+ environment,
+ smallryeConfigs,
+ effectiveSection,
+ mappingInfo,
+ propertyNamePrefix,
+ sectionPages);
+ }
+
+ private void smallryeProcessPropertyMappingInfo(
+ String logIndent,
+ DocletEnvironment environment,
+ SmallRyeConfigs smallryeConfigs,
+ String section,
+ SmallRyeConfigMappingInfo mappingInfo,
+ String propertyNamePrefix,
+ Map sectionPages) {
+ smallryeProcessMappingInfo(
+ logIndent + " ",
+ environment,
+ smallryeConfigs,
+ section,
+ mappingInfo,
+ propertyNamePrefix,
+ sectionPages);
+ }
+
+ private void smallryeProcessMappingInfo(
+ String logIndent,
+ DocletEnvironment environment,
+ SmallRyeConfigs smallryeConfigs,
+ String effectiveSection,
+ SmallRyeConfigMappingInfo mappingInfo,
+ String propertyNamePrefix,
+ Map sectionPages) {
+
+ // Eagerly create page, so we have the comment from the type.
+ sectionPages.computeIfAbsent(
+ effectiveSection,
+ s -> new SmallRyeConfigSectionPage(s, mappingInfo.element(), mappingInfo.typeComment()));
+
+ mappingInfo
+ .properties(environment)
+ .forEach(
+ prop ->
+ smallryeProcessProperty(
+ logIndent,
+ environment,
+ smallryeConfigs,
+ mappingInfo,
+ effectiveSection,
+ prop,
+ propertyNamePrefix,
+ sectionPages));
+ }
+
+ private void smallryeProcessProperty(
+ String logIndent,
+ DocletEnvironment environment,
+ SmallRyeConfigs smallryeConfigs,
+ SmallRyeConfigMappingInfo mappingInfo,
+ String section,
+ SmallRyeConfigPropertyInfo propertyInfo,
+ String propertyNamePrefix,
+ Map sectionPages) {
+
+ var effectiveSection =
+ propertyInfo.prefixOverride().map(o -> concatWithDot(section, o)).orElse(section);
+
+ var md = new MarkdownPropertyFormatter(propertyInfo);
+ if (md.isHidden()) {
+ return;
+ }
+ var fullName = formatPropertyName(propertyNamePrefix, md.propertyName(), md.propertySuffix());
+
+ var page =
+ sectionPages.computeIfAbsent(
+ effectiveSection,
+ s -> {
+ DocCommentTree doc =
+ propertyInfo.sectionDocFromType()
+ ? propertyInfo
+ .groupType()
+ .map(smallryeConfigs::getConfigMappingInfo)
+ .map(SmallRyeConfigMappingInfo::typeComment)
+ .orElse(null)
+ : propertyInfo.doc();
+ return new SmallRyeConfigSectionPage(s, mappingInfo.element(), doc);
+ });
+ propertyInfo.prefixOverride().ifPresent(o -> page.incrementSectionRef());
+ if (propertyInfo.isSettableType()) {
+ page.addProperty(fullName, propertyInfo, md);
+ }
+
+ propertyInfo
+ .groupType()
+ .ifPresent(
+ groupType ->
+ smallryeProcessPropertyMappingInfo(
+ logIndent + " ",
+ environment,
+ smallryeConfigs,
+ effectiveSection,
+ smallryeConfigs.getConfigMappingInfo(groupType),
+ fullName,
+ sectionPages));
+ }
+
+ private String formatPropertyName(
+ String propertyNamePrefix, String propertyName, String propertySuffix) {
+ var r = concatWithDot(propertyNamePrefix, propertyName);
+ return propertySuffix.isEmpty() ? r : concatWithDot(r, "`_`<" + propertySuffix + ">`_`");
+ }
+
+ private String safeFileName(String str) {
+ var sb = new StringBuilder();
+ var len = str.length();
+ var hadLOD = false;
+ for (int i = 0; i < len; i++) {
+ var c = str.charAt(i);
+ if (Character.isLetterOrDigit(c)) {
+ sb.append(c);
+ hadLOD = true;
+ } else {
+ if (hadLOD) {
+ sb.append('_');
+ hadLOD = false;
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public Set extends Option> getSupportedOptions() {
+ return Set.of(directoryOption, doctitleDummy, windowtitleDummy, notimestampDummy);
+ }
+
+ @Override
+ public String getName() {
+ return "PolarisReferenceDocsDoclet";
+ }
+
+ @Override
+ public void init(Locale locale, Reporter reporter) {}
+
+ static final class DummyOption implements Option {
+ private final List names;
+ private final int argumentCount;
+
+ DummyOption(List names, int argumentCount) {
+ this.names = names;
+ this.argumentCount = argumentCount;
+ }
+
+ @Override
+ public boolean process(String option, List arguments) {
+ return true;
+ }
+
+ @Override
+ public String getParameters() {
+ return "";
+ }
+
+ @Override
+ public List getNames() {
+ return names;
+ }
+
+ @Override
+ public Kind getKind() {
+ return null;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Ignored";
+ }
+
+ @Override
+ public int getArgumentCount() {
+ return argumentCount;
+ }
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownFormatter.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownFormatter.java
new file mode 100644
index 0000000000..a08abcf99d
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownFormatter.java
@@ -0,0 +1,634 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import static java.util.Objects.requireNonNull;
+
+import com.sun.source.doctree.AttributeTree;
+import com.sun.source.doctree.DeprecatedTree;
+import com.sun.source.doctree.DocCommentTree;
+import com.sun.source.doctree.DocTree;
+import com.sun.source.doctree.EndElementTree;
+import com.sun.source.doctree.EntityTree;
+import com.sun.source.doctree.ErroneousTree;
+import com.sun.source.doctree.HiddenTree;
+import com.sun.source.doctree.IndexTree;
+import com.sun.source.doctree.LinkTree;
+import com.sun.source.doctree.LiteralTree;
+import com.sun.source.doctree.ReferenceTree;
+import com.sun.source.doctree.SeeTree;
+import com.sun.source.doctree.SinceTree;
+import com.sun.source.doctree.StartElementTree;
+import com.sun.source.doctree.SummaryTree;
+import com.sun.source.doctree.TextTree;
+import com.sun.source.doctree.UnknownBlockTagTree;
+import com.sun.source.doctree.UnknownInlineTagTree;
+import com.sun.source.doctree.ValueTree;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.VariableElement;
+import org.apache.commons.text.StringEscapeUtils;
+
+public abstract class MarkdownFormatter {
+ private final Element element;
+ private String javadocDeprecated;
+ private String javadocSee;
+ private String javadocHidden;
+ private String javadocSince;
+ private String javadocSummary;
+ private String javadocBody;
+
+ public MarkdownFormatter(Element element, DocCommentTree commentTree) {
+ this.element = element;
+
+ if (commentTree != null) {
+
+ // @see, @deprecated, etc
+ for (DocTree doc : commentTree.getBlockTags()) {
+ switch (doc.getKind()) {
+ case SEE:
+ {
+ // @see
+ var seeTree = (SeeTree) doc;
+ var reference = seeTree.getReference();
+ this.javadocSee = format(reference);
+ }
+ break;
+ case DEPRECATED:
+ {
+ // @deprecated
+ var deprecatedTree = (DeprecatedTree) doc;
+ var body = deprecatedTree.getBody();
+ this.javadocDeprecated = format(body);
+ }
+ break;
+ case HIDDEN:
+ {
+ // @hidden
+ var hiddenTree = (HiddenTree) doc;
+ var body = hiddenTree.getBody();
+ this.javadocHidden = format(body);
+ }
+ break;
+ case SINCE:
+ {
+ // @since
+ var sinceTree = (SinceTree) doc;
+ var body = sinceTree.getBody();
+ this.javadocSince = format(body);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ var fullBody = commentTree.getFullBody();
+ if (!fullBody.isEmpty()) {
+ var body =
+ switch (fullBody.getFirst().getKind()) {
+ case SUMMARY -> {
+ var summaryTree = (SummaryTree) fullBody.getFirst();
+ this.javadocSummary = format(summaryTree.getSummary());
+ yield fullBody.subList(1, fullBody.size());
+ }
+ default -> {
+ this.javadocSummary = format(commentTree.getFirstSentence());
+ yield commentTree.getBody();
+ }
+ };
+ this.javadocBody = format(body);
+ }
+ }
+ }
+
+ public String description() {
+ var sb = new StringBuilder();
+ if (javadocSummary != null) {
+ sb.append(javadocSummary);
+ }
+ if (javadocBody != null) {
+ sb.append(' ').append(javadocBody);
+ }
+ if (javadocSince != null) {
+ sb.append("\n\nSince: ").append(javadocSince);
+ }
+ if (javadocSee != null) {
+ sb.append("\n\nSee: ").append(javadocSee);
+ }
+ if (javadocDeprecated != null) {
+ sb.append("\n\n_Deprecated_ ").append(javadocDeprecated);
+ } else if (element != null) {
+ var deprecated = element.getAnnotation(Deprecated.class) != null;
+ if (deprecated) {
+ sb.append("\n\n_Deprecated_ ");
+ }
+ }
+ return sb.toString();
+ }
+
+ public boolean isHidden() {
+ return javadocHidden != null;
+ }
+
+ private String format(List extends DocTree> docTrees) {
+ var s = new MDFormat().formatList(docTrees);
+ do {
+ var r = s.replaceAll("\n\n\n", "\n\n");
+ if (r.equals(s)) {
+ return s;
+ }
+ s = r;
+ } while (true);
+ }
+
+ static class RootTarget extends Target {
+ RootTarget() {
+ super("");
+ }
+ }
+
+ abstract static class ListTarget extends Target {
+ final String itemPrefix;
+
+ public ListTarget(String itemPrefix, String indent) {
+ super(indent);
+ this.itemPrefix = itemPrefix;
+ }
+
+ ListItemTarget newItem() {
+ return new ListItemTarget(indent);
+ }
+ }
+
+ static class OrderedListTarget extends ListTarget {
+ OrderedListTarget(String indent) {
+ super("\n" + indent + " 1. ", indent + " ");
+ }
+ }
+
+ static class UnorderedListTarget extends ListTarget {
+ UnorderedListTarget(String indent) {
+ super("\n" + indent + " * ", indent + " ");
+ }
+ }
+
+ static class ListItemTarget extends Target {
+ ListItemTarget(String indent) {
+ super(indent);
+ }
+ }
+
+ static class ATagTarget extends Target {
+ final Map attributes;
+
+ ATagTarget(String indent, Map attributes) {
+ super(indent);
+ this.attributes = attributes;
+ }
+ }
+
+ abstract static class Target {
+ final StringBuilder text = new StringBuilder();
+ final String indent;
+
+ Target(String indent) {
+ this.indent = indent;
+ }
+
+ void addText(String text) {
+ addTextOrCode(text, false);
+ }
+
+ void addTextOrCode(String text, boolean code) {
+ var t = text.replaceAll("\n", " ");
+ t = t.replaceFirst("^\\s*", "");
+
+ if (t.isEmpty() && !text.isEmpty()) {
+ maybeAddSeparator();
+ return;
+ }
+
+ var e = text.replaceFirst("\\s*$", "");
+
+ if (text.charAt(0) != t.charAt(0)) {
+ maybeAddSeparator();
+ }
+
+ if (code) {
+ this.text.append('`');
+ }
+ this.text.append(t);
+ if (code) {
+ this.text.append('`');
+ }
+
+ if (!e.equals(t)) {
+ maybeAddSeparator();
+ }
+ }
+
+ void addCode(String text) {
+ addTextOrCode(text, true);
+ }
+
+ void maybeAddSeparator() {
+ var len = text.length();
+ if (len == 0) {
+ return;
+ }
+ if (Character.isWhitespace(text.charAt(len - 1))) {
+ return;
+ }
+ text.append(' ');
+ }
+
+ void trimRight() {
+ var l = text.length();
+ while (l > 0 && Character.isWhitespace(text.charAt(l - 1))) {
+ text.setLength(--l);
+ }
+ }
+ }
+
+ private class MDFormat {
+ final Deque stack = new ArrayDeque<>();
+
+ String formatList(List extends DocTree> docTrees) {
+ var root = new RootTarget();
+ stack.add(root);
+ process(docTrees);
+ return root.text.toString();
+ }
+
+ private void process(List extends DocTree> docTrees) {
+ for (var docTree : docTrees) {
+ process(docTree);
+ }
+ }
+
+ private void process(DocTree doc) {
+ var target = requireNonNull(stack.peekLast());
+ switch (doc.getKind()) {
+ case DOC_COMMENT:
+ {
+ var docCommentTree = (DocCommentTree) doc;
+
+ var first = docCommentTree.getFirstSentence();
+ for (var ch : first) {
+ process(ch);
+ }
+ var body = docCommentTree.getBody();
+ for (var ch : body) {
+ process(ch);
+ }
+
+ // `block` has all the `@see` and such
+ var block = docCommentTree.getBlockTags();
+ process(block);
+ }
+ break;
+ case COMMENT:
+ {
+ // CommentTree commentTree = (CommentTree) doc;
+ // String body = commentTree.getBody();
+ // target.text.append(body);
+ }
+ break;
+
+ case CODE:
+ {
+ // @code
+ var literalTree = (LiteralTree) doc;
+ var body = literalTree.getBody();
+ target.addCode(body.getBody());
+ }
+ break;
+ case LINK:
+ {
+ // @link
+ link((LinkTree) doc, target, true);
+ }
+ break;
+ case LINK_PLAIN:
+ {
+ // @linkplain
+ link((LinkTree) doc, target, false);
+ }
+ break;
+ case VALUE:
+ {
+ // @value
+ value((ValueTree) doc, target);
+ }
+ break;
+ case INDEX:
+ {
+ // @index
+ var indexTree = (IndexTree) doc;
+ // var description = indexTree.getDescription();
+ var searchTerm = indexTree.getSearchTerm();
+ process(searchTerm);
+ }
+ break;
+ case SUMMARY:
+ {
+ // @summary (alternative to first sentence)
+ var summaryTree = (SummaryTree) doc;
+ var summary = summaryTree.getSummary();
+ // no special handling here
+ process(summary);
+ }
+ break;
+ case DOC_ROOT:
+ case DOC_TYPE:
+ case INHERIT_DOC:
+ // ignored "inline" tags
+ break;
+
+ case ENTITY:
+ {
+ // HTML entity
+ var entityTree = (EntityTree) doc;
+ var unescaped = StringEscapeUtils.unescapeHtml4(entityTree.toString());
+ target.addText(unescaped);
+ }
+ break;
+ case IDENTIFIER:
+ {
+ // identifier
+ // IdentifierTree identifierTree = (IdentifierTree) doc;
+ // Name name = identifierTree.getName();
+ }
+ break;
+ case REFERENCE:
+ {
+ // reference tree
+ var referenceTree = (ReferenceTree) doc;
+ var signature = referenceTree.getSignature();
+ target.text.append(signature);
+ }
+ break;
+
+ case TEXT:
+ {
+ var textTree = (TextTree) doc;
+ // TODO process HTML entities ?
+ target.addText(textTree.getBody());
+ }
+ break;
+ case LITERAL:
+ {
+ var literalTree = (LiteralTree) doc;
+ var textTree = literalTree.getBody();
+ target.addText(textTree.getBody());
+ }
+ break;
+ case ERRONEOUS:
+ {
+ // invalid text
+ var erroneousTree = (ErroneousTree) doc;
+ target.addText(erroneousTree.getBody());
+ }
+ break;
+
+ case UNKNOWN_BLOCK_TAG:
+ {
+ var unknownBlockTagTree = (UnknownBlockTagTree) doc;
+ var content = unknownBlockTagTree.getContent();
+ process(content);
+ }
+ break;
+ case UNKNOWN_INLINE_TAG:
+ {
+ var unknownInlineTagTree = (UnknownInlineTagTree) doc;
+ var content = unknownInlineTagTree.getContent();
+ process(content);
+ }
+ break;
+ case START_ELEMENT:
+ {
+ // start HTML element
+ var startElementTree = (StartElementTree) doc;
+ var name = startElementTree.getName();
+ var attributes = startElementTree.getAttributes();
+ // boolean selfClosing = startElementTree.isSelfClosing();
+
+ var attributeMap =
+ attributes.stream()
+ .filter(d -> d.getKind() == DocTree.Kind.ATTRIBUTE)
+ .map(AttributeTree.class::cast)
+ .collect(
+ Collectors.toMap(
+ a -> a.getName().toString().toLowerCase(Locale.ROOT),
+ a -> format(a.getValue())));
+
+ switch (name.toString().toLowerCase(Locale.ROOT)) {
+ case "p":
+ target.text.append("\n\n").append(target.indent);
+ break;
+ case "a":
+ target.text.append('[');
+ stack.addLast(new ATagTarget(target.indent, attributeMap));
+ break;
+ case "em":
+ case "i":
+ target.text.append('_');
+ break;
+ case "b":
+ target.text.append("**");
+ break;
+ case "ol":
+ target.text.append("\n\n");
+ stack.addLast(new OrderedListTarget(target.indent));
+ break;
+ case "ul":
+ target.text.append("\n\n");
+ stack.addLast(new UnorderedListTarget(target.indent));
+ break;
+ case "li":
+ while (!(target instanceof ListTarget listTarget)) {
+ var last = stack.removeLast();
+ target = stack.peekLast();
+ requireNonNull(target).text.append(last.text);
+ }
+ target.text.append(listTarget.itemPrefix);
+ stack.addLast(listTarget.newItem());
+ break;
+ case "code":
+ target.text.append('`');
+ break;
+ default:
+ break;
+ }
+
+ process(attributes);
+ }
+ break;
+ case END_ELEMENT:
+ {
+ // end HTML element
+ var endElementTree = (EndElementTree) doc;
+ var name = endElementTree.getName();
+ switch (name.toString().toLowerCase(Locale.ROOT)) {
+ case "p":
+ // noop
+ break;
+ case "a":
+ var a = (ATagTarget) stack.removeLast();
+ target = stack.peekLast();
+ requireNonNull(target).addText(a.text.toString());
+ target.text.append("](").append(a.attributes.get("href")).append(")");
+ break;
+ case "em":
+ case "i":
+ target.trimRight();
+ target.text.append('_');
+ break;
+ case "b":
+ target.trimRight();
+ target.text.append("**");
+ break;
+ case "ol":
+ case "ul":
+ while (!(target instanceof ListTarget)) {
+ var last = stack.removeLast();
+ target = stack.peekLast();
+ requireNonNull(target).text.append(last.text);
+ }
+ var list = stack.removeLast();
+ target = stack.peekLast();
+ requireNonNull(target).text.append(list.text).append("\n\n");
+ break;
+ case "li":
+ while (!(target instanceof ListTarget)) {
+ var last = stack.removeLast();
+ target = stack.peekLast();
+ requireNonNull(target).text.append(last.text);
+ }
+ requireNonNull(stack.peekLast()).text.append(stack.removeLast().text);
+ break;
+ case "code":
+ target.text.append('`');
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ case ATTRIBUTE:
+ // Attribute of an HTML tag
+ break;
+
+ // "block" tags
+ case AUTHOR:
+ case DEPRECATED:
+ case EXCEPTION:
+ case HIDDEN:
+ case PARAM:
+ case PROVIDES:
+ case RETURN:
+ case SEE:
+ case SERIAL:
+ case SERIAL_DATA:
+ case SERIAL_FIELD:
+ case SINCE:
+ case THROWS:
+ case USES:
+ case VERSION:
+ // ignored "block" tags
+ break;
+
+ case OTHER:
+ default:
+ break;
+ }
+ }
+
+ private void value(ValueTree valueTree, Target target) {
+ // TODO resolve reference properly
+ var reference = valueTree.getReference();
+ var signature = reference.getSignature().trim();
+ if (signature.startsWith("#") && element != null) {
+ var referenced =
+ element.getEnclosingElement().getEnclosedElements().stream()
+ .filter(enc -> enc.getSimpleName().toString().equals(signature.substring(1)))
+ .findFirst();
+ if (referenced.isPresent()) {
+ var ref = referenced.get();
+ if (ref instanceof VariableElement variableElement) {
+ var value = variableElement.getConstantValue();
+ if (value instanceof String) {
+ target.text.append('"').append(value).append('"');
+ } else {
+ target.text.append(value);
+ }
+ }
+ } else {
+ target.text.append(signature);
+ }
+ } else {
+ process(reference);
+ }
+ }
+
+ private void link(LinkTree linkTree, Target target, boolean codeValue) {
+ // TODO resolve reference properly
+
+ var label = linkTree.getLabel();
+ if (!label.isEmpty()) {
+ process(label);
+ }
+
+ var reference = linkTree.getReference();
+ var signature = reference.getSignature().trim();
+ target.maybeAddSeparator();
+ target.text.append("(");
+ if (codeValue) {
+ target.text.append('`');
+ }
+ if (signature.startsWith("#") && element != null) {
+ var referenced =
+ element.getEnclosingElement().getEnclosedElements().stream()
+ .filter(enc -> enc.getSimpleName().toString().equals(signature.substring(1)))
+ .findFirst();
+ if (referenced.isPresent()) {
+ var ref = referenced.get();
+ if (ref instanceof VariableElement variableElement) {
+ var value = variableElement.getConstantValue();
+ target.text.append(value);
+ }
+ } else {
+ target.text.append(signature);
+ }
+ } else {
+ process(reference);
+ }
+ if (codeValue) {
+ target.text.append('`');
+ }
+ target.text.append(')');
+ }
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownPropertyFormatter.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownPropertyFormatter.java
new file mode 100644
index 0000000000..d434748d04
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownPropertyFormatter.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+public class MarkdownPropertyFormatter extends MarkdownFormatter {
+
+ private final PropertyInfo propertyInfo;
+
+ public MarkdownPropertyFormatter(PropertyInfo propertyInfo) {
+ super(propertyInfo.propertyElement(), propertyInfo.doc());
+ this.propertyInfo = propertyInfo;
+ }
+
+ public String propertyName() {
+ return propertyInfo.propertyName();
+ }
+
+ public String propertySuffix() {
+ return propertyInfo.propertySuffix();
+ }
+
+ public String propertyType() {
+ return '`' + propertyInfo.simplifiedTypeName() + '`';
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownTypeFormatter.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownTypeFormatter.java
new file mode 100644
index 0000000000..001f16aac6
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/MarkdownTypeFormatter.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import javax.lang.model.element.Element;
+
+public class MarkdownTypeFormatter extends MarkdownFormatter {
+
+ public MarkdownTypeFormatter(Element element, DocCommentTree commentTree) {
+ super(element, commentTree);
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigItem.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigItem.java
new file mode 100644
index 0000000000..02041cb3ad
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigItem.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import java.util.Optional;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.VariableElement;
+
+public class PropertiesConfigItem implements PropertyInfo {
+ private final VariableElement field;
+ private final DocCommentTree doc;
+
+ public PropertiesConfigItem(VariableElement field, DocCommentTree doc) {
+ this.field = field;
+ this.doc = doc;
+ }
+
+ @Override
+ public Element propertyElement() {
+ return field;
+ }
+
+ @Override
+ public DocCommentTree doc() {
+ return doc;
+ }
+
+ @Override
+ public String defaultValue() {
+ return "";
+ }
+
+ @Override
+ public String simplifiedTypeName() {
+ return "";
+ }
+
+ @Override
+ public String propertyName() {
+ return field.getConstantValue().toString();
+ }
+
+ @Override
+ public String propertySuffix() {
+ return "";
+ }
+
+ @Override
+ public Optional> groupType() {
+ return Optional.empty();
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigPageGroup.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigPageGroup.java
new file mode 100644
index 0000000000..91f7bb9f2f
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigPageGroup.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import static javax.lang.model.element.ElementKind.FIELD;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import javax.lang.model.element.ElementVisitor;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.AbstractElementVisitor8;
+import jdk.javadoc.doclet.DocletEnvironment;
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+
+public class PropertiesConfigPageGroup {
+
+ public static final Set EXPECTED_FIELD_MODIFIERS =
+ Set.of(Modifier.FINAL, Modifier.STATIC);
+
+ private final String name;
+
+ private final Map> items = new LinkedHashMap<>();
+
+ public PropertiesConfigPageGroup(String name) {
+ this.name = name;
+ }
+
+ public Map> sectionItems() {
+ var sections = new LinkedHashMap>();
+ forEachSection(sections::put);
+ return sections;
+ }
+
+ public void forEachSection(BiConsumer> consumer) {
+ var processedSections = new HashSet();
+
+ for (var e : items.entrySet()) {
+ var section = e.getKey();
+ if (processedSections.add(section)) {
+ consumer.accept(section, e.getValue());
+ }
+ }
+ }
+
+ public String name() {
+ return name;
+ }
+
+ ElementVisitor visitor() {
+ return new AbstractElementVisitor8<>() {
+ @Override
+ public Void visitPackage(PackageElement e, DocletEnvironment env) {
+ return null;
+ }
+
+ @Override
+ public Void visitType(TypeElement e, DocletEnvironment env) {
+ return e.accept(this, env);
+ }
+
+ @Override
+ public Void visitVariable(VariableElement e, DocletEnvironment env) {
+ var configItem = e.getAnnotation(ConfigItem.class);
+ if (configItem != null) {
+ var propertyKey = e.getConstantValue();
+ if (e.getKind() == FIELD
+ && e.getModifiers().containsAll(EXPECTED_FIELD_MODIFIERS)
+ && propertyKey instanceof String) {
+ var docComment = env.getDocTrees().getDocCommentTree(e);
+
+ items
+ .computeIfAbsent(configItem.section(), g -> new ArrayList<>())
+ .add(new PropertiesConfigItem(e, docComment));
+ }
+ // else: @ConfigItem placed on something else than a constant String field.
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitExecutable(ExecutableElement e, DocletEnvironment env) {
+ return null;
+ }
+
+ @Override
+ public Void visitTypeParameter(TypeParameterElement e, DocletEnvironment env) {
+ return null;
+ }
+ };
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigs.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigs.java
new file mode 100644
index 0000000000..2bf607d1c1
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertiesConfigs.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.lang.model.element.ElementVisitor;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.AbstractElementVisitor8;
+import jdk.javadoc.doclet.DocletEnvironment;
+import org.apache.polaris.docs.ConfigDocs.ConfigPageGroup;
+
+public class PropertiesConfigs {
+ private final DocletEnvironment env;
+ private final Map pages = new HashMap<>();
+
+ public PropertiesConfigs(DocletEnvironment env) {
+ this.env = env;
+ }
+
+ public PropertiesConfigPageGroup page(String name) {
+ return pages.computeIfAbsent(name, PropertiesConfigPageGroup::new);
+ }
+
+ public Iterable pages() {
+ return pages.values();
+ }
+
+ public ElementVisitor visitor() {
+ return new AbstractElementVisitor8<>() {
+ @Override
+ public Void visitPackage(PackageElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitType(TypeElement e, Void ignore) {
+ ConfigPageGroup configPageGroup = e.getAnnotation(ConfigPageGroup.class);
+ if (configPageGroup != null) {
+ PropertiesConfigPageGroup page = page(configPageGroup.name());
+
+ e.getEnclosedElements().forEach(element -> element.accept(page.visitor(), env));
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitVariable(VariableElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitExecutable(ExecutableElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitTypeParameter(TypeParameterElement e, Void ignore) {
+ return null;
+ }
+ };
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertyInfo.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertyInfo.java
new file mode 100644
index 0000000000..5c8fa72f07
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/PropertyInfo.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import java.util.Optional;
+import javax.lang.model.element.Element;
+
+public interface PropertyInfo {
+ Element propertyElement();
+
+ String propertyName();
+
+ String propertySuffix();
+
+ String simplifiedTypeName();
+
+ String defaultValue();
+
+ DocCommentTree doc();
+
+ Optional> groupType();
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/ReferenceConfigDocsGenerator.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/ReferenceConfigDocsGenerator.java
new file mode 100644
index 0000000000..a891a25c56
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/ReferenceConfigDocsGenerator.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import static javax.tools.DocumentationTool.Location.DOCUMENTATION_OUTPUT;
+import static javax.tools.JavaFileObject.Kind.SOURCE;
+import static javax.tools.StandardLocation.CLASS_PATH;
+import static javax.tools.StandardLocation.SOURCE_PATH;
+
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import javax.tools.DiagnosticListener;
+import javax.tools.DocumentationTool;
+import javax.tools.JavaFileObject;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+/**
+ * Tool to run {@link DocGenDoclet}, the markdown docs generation.
+ *
+ * This is built as a separate tool, because running {@link DocGenDoclet} inside {@code javadoc}
+ * leads to wrong and incomplete results for smallrye-config documentation due to class loader
+ * isolation issues. {@code javadoc} uses a separate {@link ClassLoader} for the doclet, which
+ * breaks the proper inspection via smallrye-config's using {@link
+ * io.smallrye.config.ConfigMappingInterface}: the default values are missing, properties from
+ * supertypes are missing and the property names are usually wrong.
+ *
+ *
This separate tool approach makes the integration into Gradle easier, too.
+ */
+@Command(
+ name = "generate",
+ mixinStandardHelpOptions = true,
+ description = "Generate markdown documentation")
+public class ReferenceConfigDocsGenerator implements Callable {
+ @Option(
+ names = {"-cp", "--classpath"},
+ arity = "*",
+ split = ":")
+ List classpath = new ArrayList<>();
+
+ @Option(
+ names = {"-sp", "--sourcepath"},
+ arity = "*",
+ split = ":")
+ List sourcepath = new ArrayList<>();
+
+ @Option(
+ names = {"-d", "--destination"},
+ arity = "1",
+ required = true)
+ Path output;
+
+ @Option(names = {"-v", "--verbose"})
+ boolean verbose;
+
+ public ReferenceConfigDocsGenerator() {}
+
+ public ReferenceConfigDocsGenerator(
+ List sourcepath, List classpath, Path output, boolean verbose) {
+ this.classpath = classpath;
+ this.sourcepath = sourcepath;
+ this.output = output;
+ this.verbose = verbose;
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ var docTool =
+ ServiceLoader.load(DocumentationTool.class)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException("No DocumentationTool instance present"));
+
+ Files.createDirectories(output);
+
+ var fileManager = docTool.getStandardFileManager(null, null, null);
+ fileManager.setLocationFromPaths(SOURCE_PATH, sourcepath);
+ fileManager.setLocationFromPaths(CLASS_PATH, classpath);
+ fileManager.setLocationFromPaths(DOCUMENTATION_OUTPUT, List.of(output));
+
+ var sourceFiles = fileManager.list(SOURCE_PATH, "", Set.of(SOURCE), true);
+
+ var options =
+ List.of(
+ "--ignore-source-errors",
+ "-Xmaxwarns",
+ verbose ? "100" : "1",
+ "-Xmaxerrs",
+ verbose ? "100" : "1",
+ "-d",
+ output.toString());
+
+ DiagnosticListener diagnostics = verbose ? null : diagnostic -> {};
+ var out = verbose ? null : Writer.nullWriter();
+
+ var docTask =
+ docTool.getTask(out, fileManager, diagnostics, DocGenDoclet.class, options, sourceFiles);
+
+ var result = docTask.call();
+
+ return result ? 0 : 1;
+ }
+
+ public static void main(String[] args) {
+ var tool = new ReferenceConfigDocsGenerator();
+ var commandLine =
+ new CommandLine(tool)
+ .setExecutionExceptionHandler(
+ (ex, cmd, parseResult) -> {
+ // Print the full stack trace in all other cases.
+ cmd.getErr().println(cmd.getColorScheme().richStackTraceString(ex));
+ return cmd.getExitCodeExceptionMapper() != null
+ ? cmd.getExitCodeExceptionMapper().getExitCode(ex)
+ : cmd.getCommandSpec().exitCodeOnExecutionException();
+ });
+ System.exit(commandLine.execute(args));
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigMappingInfo.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigMappingInfo.java
new file mode 100644
index 0000000000..f7ce63a189
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigMappingInfo.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import io.smallrye.config.ConfigMapping.NamingStrategy;
+import io.smallrye.config.ConfigMappingInterface;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ElementVisitor;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.util.AbstractElementVisitor8;
+import jdk.javadoc.doclet.DocletEnvironment;
+
+public final class SmallRyeConfigMappingInfo {
+ private final String prefix;
+ private final List configMappingInterfaces = new ArrayList<>();
+ private final Map> methodExecutables = new HashMap<>();
+ private final Set propertiesMethodNameOrder = new LinkedHashSet<>();
+ private final Map methodNameToProperty =
+ new LinkedHashMap<>();
+ private DocCommentTree typeComment;
+ private TypeElement element;
+
+ SmallRyeConfigMappingInfo(String prefix) {
+ this.prefix = prefix;
+ }
+
+ public Stream properties(DocletEnvironment env) {
+ for (var configMappingInterface : configMappingInterfaces) {
+ for (var property : configMappingInterface.getProperties()) {
+ var methodName = property.getMethod().getName();
+ methodNameToProperty.putIfAbsent(methodName, property);
+ }
+ }
+
+ return propertiesMethodNameOrder.stream()
+ .map(name -> buildPropertyInfo(env, name))
+ .filter(Objects::nonNull);
+ }
+
+ String prefix() {
+ return prefix;
+ }
+
+ TypeElement element() {
+ return element;
+ }
+
+ DocCommentTree typeComment() {
+ return typeComment;
+ }
+
+ private SmallRyeConfigPropertyInfo buildPropertyInfo(DocletEnvironment env, String methodName) {
+ var property = methodNameToProperty.get(methodName);
+ if (property == null) {
+ return null;
+ }
+
+ DocCommentTree doc = null;
+
+ var executables = methodExecutables.get(methodName);
+ if (executables == null) {
+ return null;
+ }
+
+ ExecutableElement docExec = null;
+
+ for (var executable : executables) {
+ if (doc == null) {
+ doc = env.getDocTrees().getDocCommentTree(executable);
+ if (doc != null) {
+ docExec = executable;
+ }
+ }
+ }
+
+ if (docExec == null) {
+ docExec = executables.get(0);
+ }
+
+ var namingStrategy =
+ configMappingInterfaces.isEmpty()
+ ? NamingStrategy.KEBAB_CASE
+ : configMappingInterfaces.get(0).getNamingStrategy();
+ var propertyName = property.getPropertyName(namingStrategy);
+
+ return new SmallRyeConfigPropertyInfo(docExec, property, propertyName, doc);
+ }
+
+ void processType(
+ DocletEnvironment env,
+ ConfigMappingInterface configMappingInterface,
+ TypeElement typeElement) {
+ configMappingInterfaces.add(configMappingInterface);
+
+ // TODO use the order of the properties as in the source file? or define another annotation?
+
+ // Collect properties defined by the current type-element (type that declares a
+ // `@ConfigMapping`)
+ for (var property : configMappingInterface.getProperties()) {
+ var method = property.getMethod();
+ methodExecutables.putIfAbsent(method.getName(), new ArrayList<>());
+ }
+
+ typeElement.accept(executablesVisitor, null);
+ if (typeComment == null) {
+ typeComment = env.getDocTrees().getDocCommentTree(typeElement);
+ if (typeComment != null) {
+ this.element = typeElement;
+ }
+ }
+ }
+
+ final ElementVisitor executablesVisitor =
+ new AbstractElementVisitor8<>() {
+
+ @Override
+ public Void visitType(TypeElement e, Void unused) {
+ for (var enclosedElement : e.getEnclosedElements()) {
+ if (enclosedElement.asType().getKind() == TypeKind.EXECUTABLE) {
+ enclosedElement.accept(this, null);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitExecutable(ExecutableElement e, Void unused) {
+ if (e.getKind() == ElementKind.METHOD
+ && (e.getModifiers().contains(Modifier.ABSTRACT)
+ || e.getModifiers().contains(Modifier.DEFAULT))
+ && e.getParameters().isEmpty()
+ && e.getReturnType().getKind() != TypeKind.VOID) {
+ var methodName = e.getSimpleName().toString();
+ propertiesMethodNameOrder.add(methodName);
+ var methodList = methodExecutables.get(methodName);
+ if (methodList != null) {
+ methodList.add(e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitPackage(PackageElement e, Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitVariable(VariableElement e, Void unused) {
+ return null;
+ }
+
+ @Override
+ public Void visitTypeParameter(TypeParameterElement e, Void unused) {
+ return null;
+ }
+ };
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigPropertyInfo.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigPropertyInfo.java
new file mode 100644
index 0000000000..9b534fb473
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigPropertyInfo.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import io.smallrye.config.ConfigMappingInterface.Property;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import java.util.stream.Collectors;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+import org.apache.polaris.docs.ConfigDocs.ConfigPropertyName;
+
+public class SmallRyeConfigPropertyInfo implements PropertyInfo {
+ private final Property property;
+ private final String propertyName;
+ private final DocCommentTree doc;
+ private final ExecutableElement element;
+
+ SmallRyeConfigPropertyInfo(
+ ExecutableElement element, Property property, String propertyName, DocCommentTree doc) {
+ this.element = element;
+ this.property = property;
+ this.propertyName = propertyName;
+ this.doc = doc;
+ }
+
+ @Override
+ public Element propertyElement() {
+ return element;
+ }
+
+ @Override
+ public String propertyName() {
+ return propertyName;
+ }
+
+ public Optional prefixOverride() {
+ var item = element.getAnnotation(ConfigItem.class);
+ if (item != null) {
+ var section = item.section();
+ if (section != null && !section.isEmpty()) {
+ return Optional.of(section);
+ }
+ }
+ return Optional.empty();
+ }
+
+ public boolean sectionDocFromType() {
+ var item = element.getAnnotation(ConfigItem.class);
+ return item != null && item.sectionDocFromType();
+ }
+
+ @Override
+ public String propertySuffix() {
+ if (property.isMap()) {
+ var ci = element.getAnnotation(ConfigPropertyName.class);
+ return ci == null || ci.value().isEmpty() ? "name" : ci.value();
+ }
+ return "";
+ }
+
+ @Override
+ public String simplifiedTypeName() {
+ return simplifiedTypeName(property);
+ }
+
+ public boolean isSettableType() {
+ return isSettableType(property);
+ }
+
+ @Override
+ public Optional> groupType() {
+ var p = property;
+ if (p.isOptional()) {
+ p = p.asOptional().getNestedProperty();
+ }
+ if (p.isCollection()) {
+ p = p.asCollection().getElement();
+ }
+ if (p.isMap()) {
+ p = p.asMap().getValueProperty();
+ }
+ return p.isGroup()
+ ? Optional.of(p.asGroup().getGroupType().getInterfaceType())
+ : Optional.empty();
+ }
+
+ public static boolean isSettableType(Property property) {
+ if (property.isOptional()) {
+ var nested = property.asOptional().getNestedProperty();
+ return isSettableType(nested);
+ }
+ if (property.isCollection()) {
+ return true;
+ }
+ if (property.isMap()) {
+ var map = property.asMap();
+ var value = map.getValueProperty();
+ return isSettableType(value);
+ }
+ if (property.isPrimitive()) {
+ return true;
+ }
+ if (property.isLeaf()) {
+ return true;
+ }
+ if (property.isGroup()) {
+ // Represents a type that consists of multiple fields/properties, which are documented
+ // underneath this property.
+ return false;
+ }
+ throw new UnsupportedOperationException("Don't know how to handle " + property);
+ }
+
+ public static String simplifiedTypeName(Property property) {
+ if (property.isOptional()) {
+ var nested = property.asOptional().getNestedProperty();
+ return simplifiedTypeName(nested);
+ }
+ if (property.isCollection()) {
+ var coll = property.asCollection();
+ var element = coll.getElement();
+ return "list of " + simplifiedTypeName(element);
+ }
+ if (property.isMap()) {
+ var map = property.asMap();
+ var value = map.getValueProperty();
+ return simplifiedTypeName(value);
+ }
+ if (property.isPrimitive()) {
+ return property.asPrimitive().getPrimitiveType().getSimpleName();
+ }
+ if (property.isLeaf()) {
+ var leaf = property.asLeaf();
+ var rawType = leaf.getValueRawType();
+ if (rawType.isEnum()) {
+ return Arrays.stream(rawType.getEnumConstants())
+ .map(Enum.class::cast)
+ .map(Enum::name)
+ .collect(Collectors.joining(", "));
+ }
+ if (property.hasConvertWith()) {
+ // A smallrye-config converter always takes a string.
+ return "string";
+ }
+ if (rawType == OptionalInt.class) {
+ return "int";
+ }
+ if (rawType == OptionalLong.class) {
+ return "long";
+ }
+ if (rawType == OptionalDouble.class) {
+ return "double";
+ }
+ if (rawType == Boolean.class) {
+ return "boolean";
+ }
+ if (rawType == Byte.class) {
+ return "byte";
+ }
+ if (rawType == Short.class) {
+ return "short";
+ }
+ if (rawType == Integer.class) {
+ return "int";
+ }
+ if (rawType == Long.class) {
+ return "long";
+ }
+ if (rawType == Float.class) {
+ return "float";
+ }
+ if (rawType == Double.class) {
+ return "double";
+ }
+ if (rawType == Character.class) {
+ return "char";
+ }
+ if (rawType == String.class) {
+ return "string";
+ }
+ if (rawType == Duration.class) {
+ return "duration";
+ }
+ if (rawType == Instant.class) {
+ return "instant";
+ }
+ if (rawType == URI.class) {
+ return "uri";
+ }
+ if (rawType == Path.class) {
+ return "path";
+ }
+ return rawType.getSimpleName();
+ }
+ if (property.isGroup()) {
+ // Represents a type that consists of multiple fields/properties, which are documented
+ // underneath this property.
+ return "";
+ }
+ throw new UnsupportedOperationException("Don't know how to handle " + property);
+ }
+
+ @Override
+ public DocCommentTree doc() {
+ return doc;
+ }
+
+ @Override
+ public String defaultValue() {
+ return defaultValue(property);
+ }
+
+ public static String defaultValue(Property property) {
+ if (property.isOptional()) {
+ var nested = property.asOptional().getNestedProperty();
+ return defaultValue(nested);
+ }
+ if (property.isPrimitive()) {
+ var primitive = property.asPrimitive();
+ return primitive.hasDefaultValue() ? primitive.getDefaultValue() : null;
+ }
+ if (property.isLeaf()) {
+ var leaf = property.asLeaf();
+ return leaf.hasDefaultValue() ? leaf.getDefaultValue() : null;
+ }
+ return null;
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigSectionPage.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigSectionPage.java
new file mode 100644
index 0000000000..f8e3584c0f
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigSectionPage.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import com.sun.source.doctree.DocCommentTree;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import javax.lang.model.element.TypeElement;
+
+public class SmallRyeConfigSectionPage {
+ final String section;
+ final List lines = new ArrayList<>();
+ final TypeElement typeElement;
+ DocCommentTree comment;
+ boolean empty = true;
+ int sectionRef;
+
+ SmallRyeConfigSectionPage(String section, TypeElement typeElement, DocCommentTree comment) {
+ this.section = section;
+ this.typeElement = typeElement;
+ this.comment = comment;
+ }
+
+ public void addProperty(
+ String propertyFullName,
+ SmallRyeConfigPropertyInfo propertyInfo,
+ MarkdownPropertyFormatter md) {
+ if (empty) {
+ lines.add("| Property | Default Value | Type | Description |");
+ lines.add("|----------|---------------|------|-------------|");
+
+ empty = false;
+ }
+
+ var fullNameCode = ('`' + propertyFullName + '`').replaceAll("``", "");
+ var dv = propertyInfo.defaultValue();
+
+ lines.add(
+ "| "
+ + fullNameCode
+ + " | "
+ + (dv != null ? (dv.isEmpty() ? "(empty)" : '`' + dv + '`') : "")
+ + " | "
+ + md.propertyType()
+ + " | "
+ + md.description().replaceAll("\n", "
")
+ + " |");
+ }
+
+ public boolean isEmpty() {
+ return comment == null && empty;
+ }
+
+ public void writeTo(PrintWriter pw) {
+ if (comment != null) {
+ var typeFormatter = new MarkdownTypeFormatter(typeElement, comment);
+ pw.println(typeFormatter.description().trim());
+ pw.println();
+ }
+
+ lines.forEach(pw::println);
+ }
+
+ public void incrementSectionRef() {
+ if (++sectionRef > 1) {
+ comment = null;
+ }
+ }
+}
diff --git a/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigs.java b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigs.java
new file mode 100644
index 0000000000..3895f17402
--- /dev/null
+++ b/tools/config-docs/generator/src/main/java/org/apache/polaris/docs/generator/SmallRyeConfigs.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import static java.util.Arrays.asList;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.ConfigMappingInterface;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import javax.lang.model.element.ElementVisitor;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.PackageElement;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.TypeParameterElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.util.AbstractElementVisitor8;
+import javax.tools.StandardLocation;
+import jdk.javadoc.doclet.DocletEnvironment;
+
+public class SmallRyeConfigs {
+ private final DocletEnvironment env;
+
+ private final ClassLoader classLoader;
+
+ private final Map configMappingInfos = new HashMap<>();
+ private final Map configMappingByType = new HashMap<>();
+
+ public SmallRyeConfigs(DocletEnvironment env) {
+ this.classLoader = env.getJavaFileManager().getClassLoader(StandardLocation.CLASS_PATH);
+ this.env = env;
+ }
+
+ Collection configMappingInfos() {
+ return configMappingInfos.values();
+ }
+
+ static String concatWithDot(String s1, String s2) {
+ var sb = new StringBuilder();
+ var v1 = s1 != null && !s1.isEmpty();
+ var v2 = s2 != null && !s2.isEmpty();
+ if (v1) {
+ sb.append(s1);
+ }
+ if (v1 && v2) {
+ sb.append('.');
+ }
+ if (v2) {
+ sb.append(s2);
+ }
+ return sb.toString();
+ }
+
+ public SmallRyeConfigMappingInfo getConfigMappingInfo(Class> type) {
+ var typeName = type.getName();
+ var info = configMappingByType.get(typeName);
+ if (info == null) {
+ info = new SmallRyeConfigMappingInfo("");
+ configMappingByType.put(typeName, info);
+ TypeElement elem = env.getElementUtils().getTypeElement(typeName);
+ if (elem == null) {
+ throw new NullPointerException("Type " + typeName + " not found");
+ }
+ elem.accept(visitor(), null);
+ }
+ return info;
+ }
+
+ ElementVisitor visitor() {
+ return new AbstractElementVisitor8<>() {
+
+ @Override
+ public Void visitPackage(PackageElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitType(TypeElement e, Void ignore) {
+ switch (e.getKind()) {
+ case CLASS:
+ case INTERFACE:
+ var configMapping = e.getAnnotation(ConfigMapping.class);
+ SmallRyeConfigMappingInfo mappingInfo;
+ ConfigMappingInterface configMappingInterface;
+
+ var className = e.getQualifiedName().toString();
+
+ Class> clazz;
+ if (configMapping != null) {
+
+ mappingInfo =
+ configMappingInfos.computeIfAbsent(
+ configMapping.prefix(), SmallRyeConfigMappingInfo::new);
+
+ clazz = loadClass(className);
+ try {
+ configMappingInterface = ConfigMappingInterface.getConfigurationInterface(clazz);
+ } catch (RuntimeException ex) {
+ throw new RuntimeException("Failed to process mapped " + clazz, ex);
+ }
+ configMappingByType.put(
+ configMappingInterface.getInterfaceType().getName(), mappingInfo);
+ } else {
+ mappingInfo = configMappingByType.get(className);
+ if (mappingInfo == null) {
+ return null;
+ }
+ clazz = loadClass(className);
+ try {
+ configMappingInterface = ConfigMappingInterface.getConfigurationInterface(clazz);
+ } catch (RuntimeException ex) {
+ throw new RuntimeException("Failed to process implicitly added " + clazz, ex);
+ }
+ }
+
+ mappingInfo.processType(env, configMappingInterface, e);
+
+ var seen = new HashSet>();
+ var remaining = new ArrayDeque<>(asList(configMappingInterface.getSuperTypes()));
+ while (!remaining.isEmpty()) {
+ var superType = remaining.removeFirst();
+ if (!seen.add(superType.getInterfaceType())) {
+ continue;
+ }
+
+ remaining.addAll(asList(superType.getSuperTypes()));
+
+ var superTypeElement =
+ env.getElementUtils().getTypeElement(superType.getInterfaceType().getName());
+ mappingInfo.processType(env, superType, superTypeElement);
+ }
+
+ // Under some (not really understood) circumstances,
+ // ConfigMappingInterface.getSuperTypes() does not return really all extended
+ // interfaces. So we traverse the super types via reflection here, skipping all the
+ // types that have been visited above.
+ var remainingClasses = new ArrayDeque>();
+ if (clazz.getSuperclass() != null) {
+ remainingClasses.add(clazz.getSuperclass());
+ }
+ remainingClasses.addAll(asList(clazz.getInterfaces()));
+ while (!remainingClasses.isEmpty()) {
+ var c = remainingClasses.removeFirst();
+ if (!seen.add(c)) {
+ continue;
+ }
+
+ var superTypeElement = env.getElementUtils().getTypeElement(c.getName());
+ mappingInfo.processType(env, configMappingInterface, superTypeElement);
+
+ if (c.getSuperclass() != null) {
+ remainingClasses.add(c.getSuperclass());
+ }
+ remainingClasses.addAll(asList(c.getInterfaces()));
+ }
+ break;
+ default:
+ break;
+ }
+
+ return null;
+ }
+
+ private Class> loadClass(String className) {
+ try {
+ return Class.forName(className, false, classLoader);
+ } catch (ClassNotFoundException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public Void visitVariable(VariableElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitExecutable(ExecutableElement e, Void ignore) {
+ return null;
+ }
+
+ @Override
+ public Void visitTypeParameter(TypeParameterElement e, Void ignore) {
+ return null;
+ }
+ };
+ }
+}
diff --git a/tools/config-docs/generator/src/test/java/org/apache/polaris/docs/generator/TestDocGenTool.java b/tools/config-docs/generator/src/test/java/org/apache/polaris/docs/generator/TestDocGenTool.java
new file mode 100644
index 0000000000..daee695c6f
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/org/apache/polaris/docs/generator/TestDocGenTool.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.docs.generator;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+@ExtendWith(SoftAssertionsExtension.class)
+public class TestDocGenTool {
+ @InjectSoftAssertions protected SoftAssertions soft;
+
+ @Test
+ public void docGenTool(@TempDir Path dir) throws Exception {
+ var classpath =
+ Arrays.stream(System.getProperty("testing.libraries").split(":"))
+ .map(Paths::get)
+ .collect(Collectors.toList());
+
+ var tool =
+ new ReferenceConfigDocsGenerator(List.of(Paths.get("src/test/java")), classpath, dir, true);
+ var result = tool.call();
+ soft.assertThat(result).isEqualTo(0);
+
+ var fileProps = dir.resolve("props-main.md");
+ var filePropsA = dir.resolve("props-a_a_a.md");
+ var filePropsB = dir.resolve("props-b_b.md");
+ soft.assertThat(fileProps)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Description |
+ |----------|-------------|
+ | `property.one` | A property. "111" is the default value.
Some text there. |
+ | `property.four` | Four. |
+ """);
+ soft.assertThat(filePropsA)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Description |
+ |----------|-------------|
+ | `property.three` | Some (`value two`) more (`one two three`).
* foo
* bar
* baz
blah
* FOO
* BAR
* BAZ
* foo
* bar
* baz
|
+ """);
+ soft.assertThat(filePropsB)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Description |
+ |----------|-------------|
+ | `property.two` | Some summary for checkstyle.
Another property, need some words to cause a line break in the value tag here "111" for testing.
Some text there.
_Deprecated_ this is deprecated because of (`property.three`). |
+ | `property.five` | Five. |
+ """);
+
+ var fileMyPrefix = dir.resolve("smallrye-my_prefix.md");
+ var fileMyTypes = dir.resolve("smallrye-my_types.md");
+ var fileExtremelyNested = dir.resolve("smallrye-extremely_nested.md");
+ var fileVeryNested = dir.resolve("smallrye-very_nested.md");
+
+ soft.assertThat(fileMyPrefix)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ The docs for `my.prefix`.\s
+
+ * Some \s
+ * unordered \s
+ * list \s
+
+ Some more text. \s
+
+ 1. one \s
+ 1. two \s
+ 1. three
+
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `my.prefix.some-weird-name` | `some-default` | `string` | Something that configures something. |
+ | `my.prefix.some-duration` | | `duration` | A duration of something. |
+ | `my.prefix.nested.other-int` | | `int` | |
+ | `my.prefix.nested.boxed-double` | | `double` |
_Deprecated_ |
+ | `my.prefix.list-of-strings` | | `list of string` | Example & < > " € ® ©.
* ` session-iam-statements[0]= {"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"} `
* ` session-iam-statements[1]= {"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"} `
|
+ | `my.prefix.some-int-thing` | | `int` | Something int-ish. |
+ """);
+ soft.assertThat(fileMyTypes)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ Documentation for `my.types`.
+
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `my.types.string` | | `string` | |
+ | `my.types.optional-string` | | `string` | |
+ | `my.types.duration` | | `duration` | |
+ | `my.types.optional-duration` | | `duration` | |
+ | `my.types.path` | | `path` | |
+ | `my.types.optional-path` | | `path` | |
+ | `my.types.uri` | | `uri` | |
+ | `my.types.optional-uri` | | `uri` | |
+ | `my.types.instant` | | `instant` | |
+ | `my.types.optional-instant` | | `instant` | |
+ | `my.types.string-list` | | `list of string` | |
+ | `my.types.string-string-map.`_``_ | | `string` | |
+ | `my.types.string-duration-map.`_``_ | | `duration` | |
+ | `my.types.string-path-map.`_``_ | | `path` | |
+ | `my.types.optional-int` | | `int` | |
+ | `my.types.optional-long` | | `long` | |
+ | `my.types.optional-double` | | `double` | |
+ | `my.types.int-boxed` | | `int` | |
+ | `my.types.long-boxed` | | `long` | |
+ | `my.types.double-boxed` | | `double` | |
+ | `my.types.float-boxed` | | `float` | |
+ | `my.types.int-prim` | | `int` | |
+ | `my.types.long-prim` | | `long` | |
+ | `my.types.double-prim` | | `double` | |
+ | `my.types.float-prim` | | `float` | |
+ | `my.types.bool-boxed` | | `boolean` | |
+ | `my.types.bool-prim` | | `boolean` | |
+ | `my.types.enum-thing` | | `ONE, TWO, THREE` | |
+ | `my.types.optional-enum` | | `ONE, TWO, THREE` | |
+ | `my.types.list-of-enum` | | `list of ONE, TWO, THREE` | |
+ | `my.types.map-to-enum.`_``_ | | `ONE, TWO, THREE` | |
+ | `my.types.optional-bool` | | `boolean` | |
+ | `my.types.mapped-a.some-weird-name` | `some-default` | `string` | Something that configures something. |
+ | `my.types.mapped-a.some-duration` | | `duration` | A duration of something. |
+ | `my.types.mapped-a.nested.other-int` | | `int` | |
+ | `my.types.mapped-a.nested.boxed-double` | | `double` |
_Deprecated_ |
+ | `my.types.mapped-a.list-of-strings` | | `list of string` | Example & < > " € ® ©.
* ` session-iam-statements[0]= {"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"} `
* ` session-iam-statements[1]= {"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"} `
|
+ | `my.types.mapped-a.some-int-thing` | | `int` | Something int-ish. |
+ | `my.types.optional-mapped-a.some-weird-name` | `some-default` | `string` | Something that configures something. |
+ | `my.types.optional-mapped-a.some-duration` | | `duration` | A duration of something. |
+ | `my.types.optional-mapped-a.nested.other-int` | | `int` | |
+ | `my.types.optional-mapped-a.nested.boxed-double` | | `double` |
_Deprecated_ |
+ | `my.types.optional-mapped-a.list-of-strings` | | `list of string` | Example & < > " € ® ©.
* ` session-iam-statements[0]= {"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"} `
* ` session-iam-statements[1]= {"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"} `
|
+ | `my.types.optional-mapped-a.some-int-thing` | | `int` | Something int-ish. |
+ | `my.types.map-string-mapped-a.`_``_`.some-weird-name` | `some-default` | `string` | Something that configures something. |
+ | `my.types.map-string-mapped-a.`_``_`.some-duration` | | `duration` | A duration of something. |
+ | `my.types.map-string-mapped-a.`_``_`.nested.other-int` | | `int` | |
+ | `my.types.map-string-mapped-a.`_``_`.nested.boxed-double` | | `double` |
_Deprecated_ |
+ | `my.types.map-string-mapped-a.`_``_`.list-of-strings` | | `list of string` | Example & < > " € ® ©.
* ` session-iam-statements[0]= {"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"} `
* ` session-iam-statements[1]= {"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"} `
|
+ | `my.types.map-string-mapped-a.`_``_`.some-int-thing` | | `int` | Something int-ish. |
+ | `my.types.some-duration` | | `duration` | A duration of something. |
+ | `my.types.config-option-foo` | | `string` | Something that configures something. |
+ | `my.types.some-int-thing` | | `int` | Something int-ish. |
+ """);
+ soft.assertThat(fileExtremelyNested)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `extremely.nested.extremely-nested` | | `int` | Extremely nested. |
+ | `extremely.nested.nested-a1` | | `int` | A1. |
+ | `extremely.nested.nested-a2` | | `int` | A2. |
+ | `extremely.nested.nested-b1` | | `int` | B1. |
+ | `extremely.nested.nested-a11` | | `int` | A11. |
+ | `extremely.nested.nested-b12` | | `int` | B12. |
+ """);
+ soft.assertThat(fileVeryNested)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `very.nested.very-nested` | | `int` | Very nested. |
+ | `very.nested.nested-a1` | | `int` | A1. |
+ | `very.nested.nested-a2` | | `int` | A2. |
+ | `very.nested.nested-b1` | | `int` | B1. |
+ | `very.nested.nested-a11` | | `int` | A11. |
+ | `very.nested.nested-b12` | | `int` | B12. |
+ """);
+
+ var fileSectionA = dir.resolve("smallrye-my_types_Section_A.md");
+ soft.assertThat(fileSectionA)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ Another map of string to `MappedA`, in its own section.
+
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `my.types.map.py.`_``_`.some-weird-name` | `some-default` | `string` | Something that configures something. |
+ | `my.types.map.py.`_``_`.some-duration` | | `duration` | A duration of something. |
+ | `my.types.map.py.`_``_`.nested.other-int` | | `int` | |
+ | `my.types.map.py.`_``_`.nested.boxed-double` | | `double` |
_Deprecated_ |
+ | `my.types.map.py.`_``_`.list-of-strings` | | `list of string` | Example & < > " € ® ©.
* ` session-iam-statements[0]= {"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"} `
* ` session-iam-statements[1]= {"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"} `
|
+ | `my.types.map.py.`_``_`.some-int-thing` | | `int` | Something int-ish. |
+ """);
+
+ // Nested sections
+ var fileNestedRoot = dir.resolve("smallrye-nested_root.md");
+ soft.assertThat(fileNestedRoot)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ Doc for NestedSectionsRoot.
+
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `nested.root.nested-c.int-not-in-c` | | `int` | |
+ """);
+
+ var fileNestedA = dir.resolve("smallrye-nested_root_section_a.md");
+ soft.assertThat(fileNestedA)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `nested.root.nested-a.section-a.string-in-a` | | `string` | |
+ """);
+
+ var fileNestedB = dir.resolve("smallrye-nested_root_section_b.md");
+ soft.assertThat(fileNestedB)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `nested.root.nested-b.section-b.string-in-b` | | `string` | |
+ """);
+
+ var fileNestedC = dir.resolve("smallrye-nested_root_section_c.md");
+ soft.assertThat(fileNestedC)
+ .isRegularFile()
+ .content()
+ .isEqualTo(
+ """
+ | Property | Default Value | Type | Description |
+ |----------|---------------|------|-------------|
+ | `nested.root.nested-c.string-in-c` | | `string` | |
+ | `nested.root.nested-c.int-in-c` | | `int` | |
+ """);
+ }
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/properties/ConfigProps.java b/tools/config-docs/generator/src/test/java/tests/properties/ConfigProps.java
new file mode 100644
index 0000000000..59d461ef6d
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/properties/ConfigProps.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.properties;
+
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+import org.apache.polaris.docs.ConfigDocs.ConfigPageGroup;
+
+@SuppressWarnings("unused")
+@ConfigPageGroup(name = "props")
+public class ConfigProps {
+ /**
+ * A property. {@value #VALUE_ONE} is the default value.
+ *
+ * Some text there.
+ */
+ @ConfigItem public static final String CONF_ONE = "property.one";
+
+ /**
+ * Some summary for checkstyle.
+ *
+ *
Another property, need some words to cause a line break in the value tag here {@value
+ * #VALUE_ONE} for testing.
+ *
+ *
Some text there.
+ *
+ * @deprecated this is deprecated because of {@link #CONF_THREE}.
+ */
+ @ConfigItem(section = "b b")
+ @Deprecated
+ public static final String CONF_TWO = "property.two";
+
+ /**
+ * Some {@link #VALUE_TWO} more {@link #VALUE_THREE}.
+ *
+ *
+ * - foo
+ *
- bar
+ *
- baz
+ *
+ *
+ * blah
+ *
+ *
+ * - FOO
+ *
- BAR
+ *
- BAZ
+ *
+ *
+ *
+ * - foo
+ *
- bar
+ *
- baz
+ *
+ */
+ @ConfigItem(section = "a a a")
+ public static final String CONF_THREE = "property.three";
+
+ /** Four. */
+ @ConfigItem public static final String CONF_FOUR = "property.four";
+
+ /** Five. */
+ @ConfigItem(section = "b b")
+ public static final String CONF_FIVE = "property.five";
+
+ public static final String VALUE_ONE = "111";
+ public static final String VALUE_TWO = "value two";
+ public static final String VALUE_THREE = "one two three";
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/properties/MoreProps.java b/tools/config-docs/generator/src/test/java/tests/properties/MoreProps.java
new file mode 100644
index 0000000000..d3b7600f08
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/properties/MoreProps.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.properties;
+
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+import org.apache.polaris.docs.ConfigDocs.ConfigPageGroup;
+
+@ConfigPageGroup(name = "more")
+public class MoreProps {
+ @ConfigItem public static final String ONE = "one";
+ @ConfigItem public static final String TWO = "two";
+ @ConfigItem public static final String THREE = "three";
+
+ /**
+ * Sum.
+ *
+ * @hidden hidden thingy
+ */
+ @ConfigItem public static final String FOUR = "four";
+
+ @ConfigItem @Deprecated public static final String FIVE = "five";
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/AllTypes.java b/tools/config-docs/generator/src/test/java/tests/smallrye/AllTypes.java
new file mode 100644
index 0000000000..de27516ae3
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/AllTypes.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithName;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalDouble;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+import org.apache.polaris.docs.ConfigDocs.ConfigPropertyName;
+
+/** Documentation for {@code my.types}. */
+@ConfigMapping(prefix = "my.types")
+public interface AllTypes extends InterfaceOne {
+ String string();
+
+ Optional optionalString();
+
+ Duration duration();
+
+ Optional optionalDuration();
+
+ Path path();
+
+ Optional optionalPath();
+
+ URI uri();
+
+ Optional optionalUri();
+
+ Instant instant();
+
+ Optional optionalInstant();
+
+ List stringList();
+
+ @ConfigPropertyName("stringkey")
+ Map stringStringMap();
+
+ @ConfigPropertyName("key2")
+ Map stringDurationMap();
+
+ Map stringPathMap();
+
+ OptionalInt optionalInt();
+
+ OptionalLong optionalLong();
+
+ OptionalDouble optionalDouble();
+
+ Integer intBoxed();
+
+ Long longBoxed();
+
+ Double doubleBoxed();
+
+ Float floatBoxed();
+
+ int intPrim();
+
+ long longPrim();
+
+ double doublePrim();
+
+ float floatPrim();
+
+ Boolean boolBoxed();
+
+ boolean boolPrim();
+
+ SomeEnum enumThing();
+
+ Optional optionalEnum();
+
+ List listOfEnum();
+
+ Map mapToEnum();
+
+ Optional optionalBool();
+
+ /** My {@code MappedA}. */
+ MappedA mappedA();
+
+ /** Optional {@code MappedA}. */
+ Optional optionalMappedA();
+
+ /** Map of string to {@code MappedA}. */
+ @ConfigPropertyName("mappy")
+ Map mapStringMappedA();
+
+ /** Another map of string to {@code MappedA}, in its own section. */
+ @ConfigItem(section = "Section A")
+ @WithName("map.py")
+ Map anotherMapStringMappedA();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/ExtremelyNested.java b/tools/config-docs/generator/src/test/java/tests/smallrye/ExtremelyNested.java
new file mode 100644
index 0000000000..5c0daebb0c
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/ExtremelyNested.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import io.smallrye.config.ConfigMapping;
+
+@ConfigMapping(prefix = "extremely.nested")
+public interface ExtremelyNested extends NestedA, NestedB {
+ /** Extremely nested. */
+ int extremelyNested();
+
+ @Override
+ int nestedA1();
+
+ @Override
+ int nestedA2();
+
+ @Override
+ int nestedB1();
+
+ @Override
+ int nestedA11();
+
+ @Override
+ int nestedB12();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceOne.java b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceOne.java
new file mode 100644
index 0000000000..4815df2614
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceOne.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import java.time.Duration;
+
+public interface InterfaceOne extends InterfaceTwo {
+ /** A duration of something. */
+ Duration someDuration();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceThree.java b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceThree.java
new file mode 100644
index 0000000000..bce374eeec
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceThree.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import java.util.OptionalInt;
+
+public interface InterfaceThree {
+ /** Something int-ish. */
+ OptionalInt someIntThing();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceTwo.java b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceTwo.java
new file mode 100644
index 0000000000..b1540fab71
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/InterfaceTwo.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface InterfaceTwo extends InterfaceThree {
+ /** Something that configures something. */
+ String configOptionFoo();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/MappedA.java b/tools/config-docs/generator/src/test/java/tests/smallrye/MappedA.java
new file mode 100644
index 0000000000..45c5999ec1
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/MappedA.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+import io.smallrye.config.WithName;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * The docs for {@code my.prefix}.
+ *
+ *
+ * - Some
+ *
- unordered
+ *
- list
+ *
+ *
+ * Some more text.
+ *
+ *
+ * - one
+ *
- two
+ *
- three
+ *
+ */
+@ConfigMapping(prefix = "my.prefix")
+public interface MappedA extends InterfaceOne {
+ @WithName("some-weird-name")
+ @WithDefault("some-default")
+ @Override
+ String configOptionFoo();
+
+ @Override
+ Duration someDuration();
+
+ OtherMapped nested();
+
+ /**
+ * Example & < > " € ® ©.
+ *
+ *
+ *
+ * session-iam-statements[0]={"Effect":"Allow", "Action":"s3:*", "Resource":"arn:aws:s3:::*/alwaysAllowed/*"}
+ *
+ *
+ * session-iam-statements[1]={"Effect":"Deny", "Action":"s3:*", "Resource":"arn:aws:s3:::*/blocked/*"}
+ *
+ *
+ */
+ List listOfStrings();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA.java
new file mode 100644
index 0000000000..dfdbb568e5
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedA extends NestedA1, NestedA2 {}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA1.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA1.java
new file mode 100644
index 0000000000..1c5ea29fff
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA1.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedA1 extends NestedA11 {
+ /** A1. */
+ int nestedA1();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA11.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA11.java
new file mode 100644
index 0000000000..94ef522c0b
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA11.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedA11 {
+ /** A11. */
+ int nestedA11();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA2.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA2.java
new file mode 100644
index 0000000000..43b27e7f91
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedA2.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedA2 extends NestedA11 {
+ /** A2. */
+ int nestedA2();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB.java
new file mode 100644
index 0000000000..8a7b9d3c31
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedB extends NestedB1, NestedA2 {}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB1.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB1.java
new file mode 100644
index 0000000000..d32e8370e4
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB1.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedB1 extends NestedA11, NestedB12 {
+ /** B1. */
+ int nestedB1();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB12.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB12.java
new file mode 100644
index 0000000000..b758cc1bcd
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedB12.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface NestedB12 {
+ /** B12. */
+ int nestedB12();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionA.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionA.java
new file mode 100644
index 0000000000..13e81a365f
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionA.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import org.apache.polaris.docs.ConfigDocs;
+
+/** Doc for NestedSectionA. */
+public interface NestedSectionA {
+ @ConfigDocs.ConfigItem(section = "section_a")
+ NestedSectionTypeA sectionA();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionB.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionB.java
new file mode 100644
index 0000000000..0da7030344
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionB.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+
+/** Doc for NestedSectionB. */
+public interface NestedSectionB {
+ @ConfigItem(section = "section_b")
+ NestedSectionTypeB sectionB();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionC.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionC.java
new file mode 100644
index 0000000000..3ef360bf00
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionC.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import org.apache.polaris.docs.ConfigDocs.ConfigItem;
+
+/** Doc for NestedSectionB. */
+public interface NestedSectionC {
+ @ConfigItem(section = "section_c")
+ String stringInC();
+
+ @ConfigItem(section = "section_c")
+ int intInC();
+
+ int intNotInC();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeA.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeA.java
new file mode 100644
index 0000000000..6378d25922
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeA.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+/** Doc for NestedSectionTypeA. */
+public interface NestedSectionTypeA {
+ String stringInA();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeB.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeB.java
new file mode 100644
index 0000000000..3a6a49ced7
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionTypeB.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+/** Doc for NestedSectionTypeB. */
+public interface NestedSectionTypeB {
+ String stringInB();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionsRoot.java b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionsRoot.java
new file mode 100644
index 0000000000..39f1505f90
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/NestedSectionsRoot.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import io.smallrye.config.ConfigMapping;
+
+/** Doc for NestedSectionsRoot. */
+@ConfigMapping(prefix = "nested.root")
+public interface NestedSectionsRoot {
+ NestedSectionA nestedA();
+
+ NestedSectionB nestedB();
+
+ NestedSectionC nestedC();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/OtherMapped.java b/tools/config-docs/generator/src/test/java/tests/smallrye/OtherMapped.java
new file mode 100644
index 0000000000..e9f82849e3
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/OtherMapped.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public interface OtherMapped {
+ int otherInt();
+
+ @Deprecated
+ Double boxedDouble();
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/SomeEnum.java b/tools/config-docs/generator/src/test/java/tests/smallrye/SomeEnum.java
new file mode 100644
index 0000000000..442743f7d8
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/SomeEnum.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+public enum SomeEnum {
+ ONE,
+ TWO,
+ THREE,
+}
diff --git a/tools/config-docs/generator/src/test/java/tests/smallrye/VeryNested.java b/tools/config-docs/generator/src/test/java/tests/smallrye/VeryNested.java
new file mode 100644
index 0000000000..dd985adbc9
--- /dev/null
+++ b/tools/config-docs/generator/src/test/java/tests/smallrye/VeryNested.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package tests.smallrye;
+
+import io.smallrye.config.ConfigMapping;
+
+@ConfigMapping(prefix = "very.nested")
+public interface VeryNested extends NestedA, NestedB {
+ /** Very nested. */
+ int veryNested();
+}
diff --git a/tools/config-docs/site/build.gradle.kts b/tools/config-docs/site/build.gradle.kts
new file mode 100644
index 0000000000..994f5ad283
--- /dev/null
+++ b/tools/config-docs/site/build.gradle.kts
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+plugins {
+ `java-library`
+}
+
+description = "Polaris site - reference docs"
+
+val genProjectPaths = listOf()
+
+val genProjects by configurations.creating
+val genSources by configurations.creating
+val doclet by configurations.creating
+
+dependencies {
+ doclet(project(":polaris-config-docs-annotations"))
+ doclet(project(":polaris-config-docs-generator"))
+ doclet(libs.smallrye.config.core)
+
+ genProjects(project(":polaris-config-docs-annotations"))
+
+ genProjectPaths.forEach { p ->
+ genProjects(project(p))
+ genSources(project(p, "mainSourceElements"))
+ }
+}
+
+val generatedMarkdownDocsDir = layout.buildDirectory.dir("generatedMarkdownDocs")
+
+val generatedMarkdownDocs = tasks.register("generatedMarkdownDocs") {
+
+ mainClass = "org.apache.polaris.docs.generator.ReferenceConfigDocsGenerator"
+
+ outputs.dir(generatedMarkdownDocsDir)
+ inputs.files(doclet)
+ inputs.files(genProjects)
+ inputs.files(genSources)
+
+ doFirst {
+ delete(generatedMarkdownDocsDir)
+ }
+
+ argumentProviders.add(CommandLineArgumentProvider {
+
+ // So, in theory, all 'org.gradle.category' attributes should use the type
+ // org.gradle.api.attributes.Category,
+ // as Category.CATEGORY_ATTRIBUTE is defined. BUT! Some attributes have an attribute type ==
+ // String.class!
+ val categoryAttributeAsString = Attribute.of("org.gradle.category", String::class.java)
+
+ val classes = genProjects.incoming.artifacts
+ .filter { a ->
+ // dependencies:
+ // org.gradle.category=library
+ val category =
+ a.variant.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
+ ?: a.variant.attributes.getAttribute(categoryAttributeAsString)
+ category != null && category.toString() == Category.LIBRARY
+ }
+ .map { a -> a.file }
+
+ val sources = genSources.incoming.artifacts
+ .filter { a ->
+ // sources:
+ // org.gradle.category=verification
+ // org.gradle.verificationtype=main-sources
+
+ val category = a.variant.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
+ val verificationType =
+ a.variant.attributes.getAttribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE)
+ category != null &&
+ category.name == Category.VERIFICATION &&
+ verificationType != null &&
+ verificationType.name == VerificationType.MAIN_SOURCES &&
+ a.file.name != "resources"
+ }
+ .map { a -> a.file }
+
+ listOf(
+ "--classpath", classes.joinToString(":"),
+ "--sourcepath", sources.joinToString(":"),
+ "--destination", generatedMarkdownDocsDir.get().toString()
+ ) + (if (logger.isInfoEnabled) listOf("--verbose") else listOf())
+ })
+
+ classpath(doclet)
+}
+
+val generateDocs by tasks.registering(Sync::class) {
+ dependsOn(generatedMarkdownDocs)
+
+ val targetDir = layout.buildDirectory.dir("markdown-docs")
+
+ outputs.dir(targetDir)
+
+ into(targetDir)
+
+ from(generatedMarkdownDocsDir)
+
+ duplicatesStrategy = DuplicatesStrategy.FAIL
+
+ doLast { delete(targetDir.get().dir("org")) }
+}