This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-table-store.git
The following commit(s) were added to refs/heads/master by this push:
new c44f0d5b [FLINK-26901] Introduce config options document for table
store
c44f0d5b is described below
commit c44f0d5beb9a772ea740d55f8545dd2a415cc51d
Author: SteNicholas <[email protected]>
AuthorDate: Mon Jul 4 10:02:34 2022 +0800
[FLINK-26901] Introduce config options document for table store
This closes #195
---
docs/content/docs/development/configuration.md | 51 ++
.../generated/catalog_configuration.html | 30 +
.../shortcodes/generated/core_configuration.html | 210 +++++++
.../generated/flink_connector_configuration.html | 30 +
.../generated/kafka_log_configuration.html | 24 +
.../store/connector/FlinkConnectorOptions.java | 4 +
.../org/apache/flink/table/store/CoreOptions.java | 2 +
flink-table-store-docs/README.md | 36 ++
flink-table-store-docs/pom.xml | 171 ++++++
.../configuration/ConfigOptionsDocGenerator.java | 632 +++++++++++++++++++++
.../docs/configuration/OptionsClassLocation.java | 46 ++
.../apache/flink/table/store/docs/util/Utils.java | 36 ++
.../src/main/resources/log4j.properties | 24 +
.../ConfigOptionsDocGeneratorTest.java | 608 ++++++++++++++++++++
.../ConfigOptionsDocsCompletenessITCase.java | 377 ++++++++++++
.../docs/configuration/data/TestCommonOptions.java | 51 ++
pom.xml | 25 +
17 files changed, 2357 insertions(+)
diff --git a/docs/content/docs/development/configuration.md
b/docs/content/docs/development/configuration.md
new file mode 100644
index 00000000..fc9caf49
--- /dev/null
+++ b/docs/content/docs/development/configuration.md
@@ -0,0 +1,51 @@
+---
+title: "Configuration"
+weight: 6
+type: docs
+aliases:
+- /development/configuration.html
+---
+<!--
+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.
+-->
+
+# Configuration
+
+**`CoreOptions`**
+
+Core options for table store.
+
+{{< generated/core_configuration >}}
+
+**`CatalogOptions`**
+
+Options for table store catalog.
+
+{{< generated/catalog_configuration >}}
+
+**`FlinkConnectorOptions`**
+
+Flink connector options for table store.
+
+{{< generated/flink_connector_configuration >}}
+
+**`KafkaLogOptions`**
+
+Kafka log options provided after configuring `log.system` for kafka.
+
+{{< generated/kafka_log_configuration >}}
diff --git a/docs/layouts/shortcodes/generated/catalog_configuration.html
b/docs/layouts/shortcodes/generated/catalog_configuration.html
new file mode 100644
index 00000000..f8991bd7
--- /dev/null
+++ b/docs/layouts/shortcodes/generated/catalog_configuration.html
@@ -0,0 +1,30 @@
+<table class="configuration table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 20%">Key</th>
+ <th class="text-left" style="width: 15%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 55%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><h5>metastore</h5></td>
+ <td style="word-wrap: break-word;">"filesystem"</td>
+ <td>String</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td><h5>uri</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Uri of metastore server.</td>
+ </tr>
+ <tr>
+ <td><h5>warehouse</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>The warehouse root path of catalog.</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/docs/layouts/shortcodes/generated/core_configuration.html
b/docs/layouts/shortcodes/generated/core_configuration.html
new file mode 100644
index 00000000..84a64f3b
--- /dev/null
+++ b/docs/layouts/shortcodes/generated/core_configuration.html
@@ -0,0 +1,210 @@
+<table class="configuration table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 20%">Key</th>
+ <th class="text-left" style="width: 15%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 55%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><h5>bucket</h5></td>
+ <td style="word-wrap: break-word;">1</td>
+ <td>Integer</td>
+ <td>Bucket number for file store.</td>
+ </tr>
+ <tr>
+ <td><h5>bucket-key</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Specify the table store distribution policy. Data is assigned
to each bucket according to the hash value of bucket-key.<br />If you specify
multiple fields, delimiter is ','.<br />If not specified, the primary key will
be used; if there is no primary key, the full row will be used.</td>
+ </tr>
+ <tr>
+ <td><h5>changelog-file</h5></td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>Whether to double write to a changelog file when flushing
memory table. This changelog file keeps the order of data input and the details
of data changes, it can be read directly during stream reads.</td>
+ </tr>
+ <tr>
+ <td><h5>commit.force-compact</h5></td>
+ <td style="word-wrap: break-word;">false</td>
+ <td>Boolean</td>
+ <td>Whether to force a compaction before commit.</td>
+ </tr>
+ <tr>
+ <td><h5>compaction.max-size-amplification-percent</h5></td>
+ <td style="word-wrap: break-word;">200</td>
+ <td>Integer</td>
+ <td>The size amplification is defined as the amount (in
percentage) of additional storage needed to store a single byte of data in the
merge tree.</td>
+ </tr>
+ <tr>
+ <td><h5>compaction.size-ratio</h5></td>
+ <td style="word-wrap: break-word;">1</td>
+ <td>Integer</td>
+ <td>Percentage flexibility while comparing sorted run size. If the
candidate sorted run(s) size is 1% smaller than the next sorted run's size,
then include next sorted run into this candidate set.</td>
+ </tr>
+ <tr>
+ <td><h5>continuous.discovery-interval</h5></td>
+ <td style="word-wrap: break-word;">1 s</td>
+ <td>Duration</td>
+ <td>The discovery interval of continuous reading.</td>
+ </tr>
+ <tr>
+ <td><h5>file.format</h5></td>
+ <td style="word-wrap: break-word;">"orc"</td>
+ <td>String</td>
+ <td>Specify the message format of data files.</td>
+ </tr>
+ <tr>
+ <td><h5>log.changelog-mode</h5></td>
+ <td style="word-wrap: break-word;">auto</td>
+ <td><p>Enum</p></td>
+ <td>Specify the log changelog mode for table.<br /><ul><li>"auto":
Upsert for table with primary key, all for table without primary
key..</li><li>"all": The log system stores all changes including
UPDATE_BEFORE.</li><li>"upsert": The log system does not store the
UPDATE_BEFORE changes, the log consumed job will automatically add the
normalized node, relying on the state to generate the required
update_before.</li></ul><br /><br />Possible values:<ul><li>"auto": Upsert for
tab [...]
+ </tr>
+ <tr>
+ <td><h5>log.consistency</h5></td>
+ <td style="word-wrap: break-word;">transactional</td>
+ <td><p>Enum</p></td>
+ <td>Specify the log consistency mode for table.<br
/><ul><li>"transactional": Only the data after the checkpoint can be seen by
readers, the latency depends on checkpoint interval.</li><li>"eventual":
Immediate data visibility, you may see some intermediate states, but eventually
the right results will be produced, only works for table with primary
key.</li></ul><br /><br />Possible values:<ul><li>"transactional": Only the
data after the checkpoint can be seen by readers, the [...]
+ </tr>
+ <tr>
+ <td><h5>log.format</h5></td>
+ <td style="word-wrap: break-word;">"debezium-json"</td>
+ <td>String</td>
+ <td>Specify the message format of log system.</td>
+ </tr>
+ <tr>
+ <td><h5>log.key.format</h5></td>
+ <td style="word-wrap: break-word;">"json"</td>
+ <td>String</td>
+ <td>Specify the key message format of log system with primary
key.</td>
+ </tr>
+ <tr>
+ <td><h5>log.retention</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Duration</td>
+ <td>It means how long changes log will be kept. The default value
is from the log system cluster.</td>
+ </tr>
+ <tr>
+ <td><h5>log.scan</h5></td>
+ <td style="word-wrap: break-word;">full</td>
+ <td><p>Enum</p></td>
+ <td>Specify the startup mode for log consumer.<br
/><ul><li>"full": Perform a snapshot on the table upon first startup, and
continue to read the latest changes.</li></ul><ul><li>"latest": Start from the
latest.</li></ul><ul><li>"from-timestamp": Start from user-supplied
timestamp.</li></ul><br /><br />Possible values:<ul><li>"full": Perform a
snapshot on the table upon first startup, and continue to read the latest
changes.</li><li>"latest": Start from the latest.</li><li>"fr [...]
+ </tr>
+ <tr>
+ <td><h5>log.scan.timestamp-millis</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Long</td>
+ <td>Optional timestamp used in case of "from-timestamp" scan
mode</td>
+ </tr>
+ <tr>
+ <td><h5>manifest.format</h5></td>
+ <td style="word-wrap: break-word;">"avro"</td>
+ <td>String</td>
+ <td>Specify the message format of manifest files.</td>
+ </tr>
+ <tr>
+ <td><h5>manifest.merge-min-count</h5></td>
+ <td style="word-wrap: break-word;">30</td>
+ <td>Integer</td>
+ <td>To avoid frequent manifest merges, this parameter specifies
the minimum number of ManifestFileMeta to merge.</td>
+ </tr>
+ <tr>
+ <td><h5>manifest.target-file-size</h5></td>
+ <td style="word-wrap: break-word;">8 mb</td>
+ <td>MemorySize</td>
+ <td>Suggested file size of a manifest file.</td>
+ </tr>
+ <tr>
+ <td><h5>merge-engine</h5></td>
+ <td style="word-wrap: break-word;">deduplicate</td>
+ <td><p>Enum</p></td>
+ <td>Specify the merge engine for table with primary key.<br
/><ul><li>"deduplicate": De-duplicate and keep the last
row.</li><li>"partial-update": Partial update non-null fields.</li></ul><br
/><br />Possible values:<ul><li>"deduplicate": De-duplicate and keep the last
row.</li><li>"partial-update": Partial update non-null fields.</li></ul></td>
+ </tr>
+ <tr>
+ <td><h5>num-levels</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Integer</td>
+ <td>Total level number, for example, there are 3 levels, including
0,1,2 levels.</td>
+ </tr>
+ <tr>
+ <td><h5>num-sorted-run.compaction-trigger</h5></td>
+ <td style="word-wrap: break-word;">5</td>
+ <td>Integer</td>
+ <td>The sorted run number to trigger compaction. Includes level0
files (one file one sorted run) and high-level runs (one level one sorted
run).</td>
+ </tr>
+ <tr>
+ <td><h5>num-sorted-run.stop-trigger</h5></td>
+ <td style="word-wrap: break-word;">10</td>
+ <td>Integer</td>
+ <td>The number of sorted-runs that trigger the stopping of
writes.</td>
+ </tr>
+ <tr>
+ <td><h5>page-size</h5></td>
+ <td style="word-wrap: break-word;">64 kb</td>
+ <td>MemorySize</td>
+ <td>Memory page size.</td>
+ </tr>
+ <tr>
+ <td><h5>partition.default-name</h5></td>
+ <td style="word-wrap: break-word;">"__DEFAULT_PARTITION__"</td>
+ <td>String</td>
+ <td>The default partition name in case the dynamic partition
column value is null/empty string.</td>
+ </tr>
+ <tr>
+ <td><h5>sequence.field</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>The field that generates the sequence number for primary key
table, the sequence number determines which data is the most recent.</td>
+ </tr>
+ <tr>
+ <td><h5>snapshot.num-retained.max</h5></td>
+ <td style="word-wrap: break-word;">2147483647</td>
+ <td>Integer</td>
+ <td>The maximum number of completed snapshots to retain.</td>
+ </tr>
+ <tr>
+ <td><h5>snapshot.num-retained.min</h5></td>
+ <td style="word-wrap: break-word;">10</td>
+ <td>Integer</td>
+ <td>The minimum number of completed snapshots to retain.</td>
+ </tr>
+ <tr>
+ <td><h5>snapshot.time-retained</h5></td>
+ <td style="word-wrap: break-word;">1 h</td>
+ <td>Duration</td>
+ <td>The maximum time of completed snapshots to retain.</td>
+ </tr>
+ <tr>
+ <td><h5>source.split.open-file-cost</h5></td>
+ <td style="word-wrap: break-word;">4 mb</td>
+ <td>MemorySize</td>
+ <td>Open file cost of a source file. It is used to avoid reading
too many files with a source split, which can be very slow.</td>
+ </tr>
+ <tr>
+ <td><h5>source.split.target-size</h5></td>
+ <td style="word-wrap: break-word;">128 mb</td>
+ <td>MemorySize</td>
+ <td>Target size of a source split when scanning a bucket.</td>
+ </tr>
+ <tr>
+ <td><h5>target-file-size</h5></td>
+ <td style="word-wrap: break-word;">128 mb</td>
+ <td>MemorySize</td>
+ <td>Target size of a file.</td>
+ </tr>
+ <tr>
+ <td><h5>write-buffer-size</h5></td>
+ <td style="word-wrap: break-word;">256 mb</td>
+ <td>MemorySize</td>
+ <td>Amount of data to build up in memory before converting to a
sorted on-disk file.</td>
+ </tr>
+ <tr>
+ <td><h5>write-mode</h5></td>
+ <td style="word-wrap: break-word;">change-log</td>
+ <td><p>Enum</p></td>
+ <td>Specify the write mode for table.<br /><ul><li>"append-only":
The table can only accept append-only insert operations. Neither data
deduplication nor any primary key constraints will be done when inserting rows
into table store.</li></ul><ul><li>"change-log": The table can accept
insert/delete/update operations.</li></ul><br /><br />Possible
values:<ul><li>"append-only": The table can only accept append-only insert
operations. Neither data deduplication nor any primary ke [...]
+ </tr>
+ </tbody>
+</table>
diff --git
a/docs/layouts/shortcodes/generated/flink_connector_configuration.html
b/docs/layouts/shortcodes/generated/flink_connector_configuration.html
new file mode 100644
index 00000000..30a9a69e
--- /dev/null
+++ b/docs/layouts/shortcodes/generated/flink_connector_configuration.html
@@ -0,0 +1,30 @@
+<table class="configuration table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 20%">Key</th>
+ <th class="text-left" style="width: 15%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 55%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><h5>log.system</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>The log system used to keep changes of the table.</td>
+ </tr>
+ <tr>
+ <td><h5>scan.parallelism</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Integer</td>
+ <td>Define a custom parallelism for the scan source. By default,
if this option is not defined, the planner will derive the parallelism for each
statement individually by also considering the global configuration.</td>
+ </tr>
+ <tr>
+ <td><h5>sink.parallelism</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>Integer</td>
+ <td>Defines a custom parallelism for the sink. By default, if this
option is not defined, the planner will derive the parallelism for each
statement individually by also considering the global configuration.</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/docs/layouts/shortcodes/generated/kafka_log_configuration.html
b/docs/layouts/shortcodes/generated/kafka_log_configuration.html
new file mode 100644
index 00000000..30914385
--- /dev/null
+++ b/docs/layouts/shortcodes/generated/kafka_log_configuration.html
@@ -0,0 +1,24 @@
+<table class="configuration table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 20%">Key</th>
+ <th class="text-left" style="width: 15%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 55%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><h5>kafka.bootstrap.servers</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Required Kafka server connection string</td>
+ </tr>
+ <tr>
+ <td><h5>kafka.topic</h5></td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Topic of this kafka table.</td>
+ </tr>
+ </tbody>
+</table>
diff --git
a/flink-table-store-connector/src/main/java/org/apache/flink/table/store/connector/FlinkConnectorOptions.java
b/flink-table-store-connector/src/main/java/org/apache/flink/table/store/connector/FlinkConnectorOptions.java
index e74a8e5d..a49a7b27 100644
---
a/flink-table-store-connector/src/main/java/org/apache/flink/table/store/connector/FlinkConnectorOptions.java
+++
b/flink-table-store-connector/src/main/java/org/apache/flink/table/store/connector/FlinkConnectorOptions.java
@@ -19,6 +19,7 @@
package org.apache.flink.table.store.connector;
import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.docs.Documentation;
import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.configuration.ConfigOptions;
import org.apache.flink.table.catalog.ObjectIdentifier;
@@ -34,6 +35,7 @@ public class FlinkConnectorOptions {
public static final String TABLE_STORE_PREFIX = "table-store.";
@Internal
+ @Documentation.ExcludeFromDocumentation("Internal use only")
public static final ConfigOption<String> ROOT_PATH =
ConfigOptions.key("root-path")
.stringType()
@@ -41,6 +43,7 @@ public class FlinkConnectorOptions {
.withDescription("The root file path of the table store in
the filesystem.");
@Internal
+ @Documentation.ExcludeFromDocumentation("Internal use only")
public static final ConfigOption<Boolean> COMPACTION_MANUAL_TRIGGERED =
ConfigOptions.key("compaction.manual-triggered")
.booleanType()
@@ -49,6 +52,7 @@ public class FlinkConnectorOptions {
"An internal flag to indicate a manual triggered
compaction job.");
@Internal
+ @Documentation.ExcludeFromDocumentation("Internal use only")
public static final ConfigOption<String> COMPACTION_PARTITION_SPEC =
ConfigOptions.key("compaction.partition-spec")
.stringType()
diff --git
a/flink-table-store-core/src/main/java/org/apache/flink/table/store/CoreOptions.java
b/flink-table-store-core/src/main/java/org/apache/flink/table/store/CoreOptions.java
index afc3c498..04a28a96 100644
---
a/flink-table-store-core/src/main/java/org/apache/flink/table/store/CoreOptions.java
+++
b/flink-table-store-core/src/main/java/org/apache/flink/table/store/CoreOptions.java
@@ -19,6 +19,7 @@
package org.apache.flink.table.store;
import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.docs.Documentation;
import org.apache.flink.configuration.ConfigOption;
import org.apache.flink.configuration.ConfigOptions;
import org.apache.flink.configuration.Configuration;
@@ -70,6 +71,7 @@ public class CoreOptions implements Serializable {
.build());
@Internal
+ @Documentation.ExcludeFromDocumentation("Internal use only")
public static final ConfigOption<String> PATH =
ConfigOptions.key("path")
.stringType()
diff --git a/flink-table-store-docs/README.md b/flink-table-store-docs/README.md
new file mode 100644
index 00000000..9e655ba8
--- /dev/null
+++ b/flink-table-store-docs/README.md
@@ -0,0 +1,36 @@
+<!--
+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.
+-->
+
+# Documentation generators
+
+This module contains generators that create HTML files directly from Flink
Table Store's source code.
+
+## Configuration documentation
+
+The `ConfigOptionsDocGenerator` can be used to generate a reference of
`ConfigOptions`. By default, a separate file is generated for each `*Options`
class found in `org.apache.flink.table.store`,
`org.apache.flink.table.store.connector` and
`org.apache.flink.table.store.kafka`.
+The `@ConfigGroups` annotation can be used to generate multiple files from a
single class.
+
+To integrate an `*Options` class from another package, add another
module-package argument pair to `ConfigOptionsDocGenerator#LOCATIONS`.
+
+The files can be generated by running `mvn package -Pgenerate-docs -pl
flink-table-store-docs -nsu -DskipTests`, and can be integrated into the
documentation using `{{ include generated/<file-name> >}}`.
+
+The documentation must be regenerated whenever
+* an `*Options` class was added or removed
+* a `ConfigOption` was added to or removed from an `*Options` class
+* a `ConfigOption` was modified in any way.
diff --git a/flink-table-store-docs/pom.xml b/flink-table-store-docs/pom.xml
new file mode 100644
index 00000000..50595f23
--- /dev/null
+++ b/flink-table-store-docs/pom.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <artifactId>flink-table-store-parent</artifactId>
+ <groupId>org.apache.flink</groupId>
+ <version>0.2-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>flink-table-store-docs</artifactId>
+ <name>Flink Table Store : Docs</name>
+
+ <properties>
+
<generated.docs.dir>./docs/layouts/shortcodes/generated</generated.docs.dir>
+ <assertj.version>3.21.0</assertj.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-table-store-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-table-store-connector</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-table-store-kafka</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-core</artifactId>
+ <version>${flink.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-table-common</artifactId>
+ <version>${flink.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Test -->
+ <dependency>
+ <!-- Used for parsing HTML -->
+ <groupId>org.jsoup</groupId>
+ <artifactId>jsoup</artifactId>
+ <version>1.14.2</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ <executions>
+ <execution>
+ <!-- Not important for this module as we only access
Flink classes-->
+ <id>dependency-convergence</id>
+ <goals>
+ <goal>enforce</goal>
+ </goals>
+ <configuration>
+ <skip>true</skip>
+ <rules>
+ <dependencyConvergence/>
+ </rules>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <rootDir>${rootDir}</rootDir>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <version>1.7</version>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>generate-docs</id>
+ <activation>
+ <activeByDefault>false</activeByDefault>
+ </activation>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-antrun-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>generate-docs</id>
+ <phase>package</phase>
+ <goals>
+ <goal>run</goal>
+ </goals>
+ <configuration>
+ <target>
+ <mkdir
dir="${rootDir}/${generated.docs.dir}"/>
+ <java
classname="org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator"
+ fork="true" failonerror="true">
+ <classpath
refid="maven.compile.classpath"/>
+ <arg
value="${rootDir}/${generated.docs.dir}/"/>
+ <arg value="${rootDir}"/>
+ </java>
+ </target>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git
a/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGenerator.java
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGenerator.java
new file mode 100644
index 00000000..225515b9
--- /dev/null
+++
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGenerator.java
@@ -0,0 +1,632 @@
+/*
+ * 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.flink.table.store.docs.configuration;
+
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.annotation.docs.ConfigGroup;
+import org.apache.flink.annotation.docs.ConfigGroups;
+import org.apache.flink.annotation.docs.Documentation;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.ConfigOption;
+import org.apache.flink.configuration.DescribedEnum;
+import org.apache.flink.configuration.description.Description;
+import org.apache.flink.configuration.description.Formatter;
+import org.apache.flink.configuration.description.HtmlFormatter;
+import org.apache.flink.configuration.description.InlineElement;
+import org.apache.flink.configuration.description.TextElement;
+import org.apache.flink.util.TimeUtils;
+import org.apache.flink.util.function.ThrowingConsumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static org.apache.flink.configuration.description.TextElement.text;
+import static org.apache.flink.table.store.docs.util.Utils.escapeCharacters;
+
+/** Class used for generating code based documentation of configuration
parameters. */
+public class ConfigOptionsDocGenerator {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ConfigOptionsDocGenerator.class);
+
+ static final OptionsClassLocation[] LOCATIONS =
+ new OptionsClassLocation[] {
+ new OptionsClassLocation("flink-table-store-core",
"org.apache.flink.table.store"),
+ new OptionsClassLocation(
+ "flink-table-store-connector",
"org.apache.flink.table.store.connector"),
+ new OptionsClassLocation(
+ "flink-table-store-kafka",
"org.apache.flink.table.store.kafka")
+ };
+ static final String DEFAULT_PATH_PREFIX = "src/main/java";
+
+ private static final String CLASS_NAME_GROUP = "className";
+ private static final String CLASS_PREFIX_GROUP = "classPrefix";
+ private static final Pattern CLASS_NAME_PATTERN =
+ Pattern.compile(
+ "(?<"
+ + CLASS_NAME_GROUP
+ + ">(?<"
+ + CLASS_PREFIX_GROUP
+ +
">[a-zA-Z]*)(?:Options|Config|Parameters))(?:\\.java)?");
+
+ private static final Formatter formatter = new HtmlFormatter();
+
+ /**
+ * This method generates html tables from set of classes containing {@link
ConfigOption
+ * ConfigOptions}.
+ *
+ * <p>For each class 1 or more html tables will be generated and placed
into a separate file,
+ * depending on whether the class is annotated with {@link ConfigGroups}.
The tables contain the
+ * key, default value and description for every {@link ConfigOption}.
+ *
+ * <p>One additional table is generated containing all {@link ConfigOption
ConfigOptions} that
+ * are annotated with {@link Documentation.Section}.
+ *
+ * @param args [0] output directory for the generated files [1] project
root directory
+ */
+ public static void main(String[] args) throws IOException,
ClassNotFoundException {
+ String outputDirectory = args[0];
+ String rootDir = args[1];
+ LOG.info(
+ "Searching the following locations; configured via
{}#LOCATIONS:{}",
+ ConfigOptionsDocGenerator.class.getCanonicalName(),
+ Arrays.stream(LOCATIONS)
+ .map(OptionsClassLocation::toString)
+ .collect(Collectors.joining("\n\t", "\n\t", "")));
+ for (OptionsClassLocation location : LOCATIONS) {
+ createTable(rootDir, location.getModule(), location.getPackage(),
outputDirectory);
+ }
+
+ generateCommonSection(rootDir, outputDirectory, LOCATIONS,
DEFAULT_PATH_PREFIX);
+ }
+
+ @VisibleForTesting
+ static void generateCommonSection(
+ String rootDir,
+ String outputDirectory,
+ OptionsClassLocation[] locations,
+ String pathPrefix)
+ throws IOException, ClassNotFoundException {
+ List<OptionWithMetaInfo> allSectionOptions = new ArrayList<>(32);
+ for (OptionsClassLocation location : locations) {
+ allSectionOptions.addAll(
+ findSectionOptions(
+ rootDir, location.getModule(),
location.getPackage(), pathPrefix));
+ }
+
+ Map<String, List<OptionWithMetaInfo>> optionsGroupedBySection =
+ allSectionOptions.stream()
+ .flatMap(
+ option -> {
+ final String[] sections =
+ option.field
+
.getAnnotation(Documentation.Section.class)
+ .value();
+ if (sections.length == 0) {
+ throw new RuntimeException(
+ String.format(
+ "Option %s is
annotated with %s but the list of sections is empty.",
+ option.option.key(),
+
Documentation.Section.class
+
.getSimpleName()));
+ }
+
+ return Arrays.stream(sections)
+ .map(section -> Tuple2.of(section,
option));
+ })
+ .collect(
+ Collectors.groupingBy(
+ option -> option.f0,
+ Collectors.mapping(
+ option -> option.f1,
Collectors.toList())));
+
+ optionsGroupedBySection.forEach(
+ (section, options) -> {
+ options.sort(
+ (o1, o2) -> {
+ int position1 =
+ o1.field
+
.getAnnotation(Documentation.Section.class)
+ .position();
+ int position2 =
+ o2.field
+
.getAnnotation(Documentation.Section.class)
+ .position();
+ if (position1 == position2) {
+ return
o1.option.key().compareTo(o2.option.key());
+ } else {
+ return Integer.compare(position1,
position2);
+ }
+ });
+
+ String sectionHtmlTable = toHtmlTable(options);
+ try {
+ Files.write(
+ Paths.get(outputDirectory,
getSectionFileName(section)),
+
sectionHtmlTable.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ @VisibleForTesting
+ static String getSectionFileName(String section) {
+ return section + "_section.html";
+ }
+
+ private static Collection<OptionWithMetaInfo> findSectionOptions(
+ String rootDir, String module, String packageName, String
pathPrefix)
+ throws IOException, ClassNotFoundException {
+ Collection<OptionWithMetaInfo> commonOptions = new ArrayList<>(32);
+ processConfigOptions(
+ rootDir,
+ module,
+ packageName,
+ pathPrefix,
+ optionsClass ->
+ extractConfigOptions(optionsClass).stream()
+ .filter(
+ optionWithMetaInfo ->
+
optionWithMetaInfo.field.getAnnotation(
+
Documentation.Section.class)
+ != null)
+ .forEachOrdered(commonOptions::add));
+ return commonOptions;
+ }
+
+ private static void createTable(
+ String rootDir, String module, String packageName, String
outputDirectory)
+ throws IOException, ClassNotFoundException {
+ processConfigOptions(
+ rootDir,
+ module,
+ packageName,
+ ConfigOptionsDocGenerator.DEFAULT_PATH_PREFIX,
+ optionsClass -> {
+ List<Tuple2<ConfigGroup, String>> tables =
generateTablesForClass(optionsClass);
+ for (Tuple2<ConfigGroup, String> group : tables) {
+ String name;
+ if (group.f0 == null) {
+ Matcher matcher =
+
CLASS_NAME_PATTERN.matcher(optionsClass.getSimpleName());
+ if (!matcher.matches()) {
+ throw new RuntimeException(
+ "Pattern did not match for "
+ + optionsClass.getSimpleName()
+ + '.');
+ }
+ name = matcher.group(CLASS_PREFIX_GROUP);
+ } else {
+ name = group.f0.name();
+ }
+ String outputFile = toSnakeCase(name) +
"_configuration.html";
+ Files.write(
+ Paths.get(outputDirectory, outputFile),
+ group.f1.getBytes(StandardCharsets.UTF_8));
+ }
+ });
+ }
+
+ @VisibleForTesting
+ static String toSnakeCase(String name) {
+ return name.replaceAll("(.)([A-Z][a-z])", "$1_$2").toLowerCase();
+ }
+
+ @VisibleForTesting
+ static void processConfigOptions(
+ String rootDir,
+ String module,
+ String packageName,
+ String pathPrefix,
+ ThrowingConsumer<Class<?>, IOException> classConsumer)
+ throws IOException, ClassNotFoundException {
+ Path configDir = Paths.get(rootDir, module, pathPrefix,
packageName.replaceAll("\\.", "/"));
+ try (DirectoryStream<Path> stream =
Files.newDirectoryStream(configDir)) {
+ for (Path entry : stream) {
+ String fileName = entry.getFileName().toString();
+ Matcher matcher = CLASS_NAME_PATTERN.matcher(fileName);
+ if (matcher.matches()) {
+ classConsumer.accept(
+ Class.forName(packageName + '.' +
matcher.group(CLASS_NAME_GROUP)));
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static List<Tuple2<ConfigGroup, String>> generateTablesForClass(Class<?>
optionsClass) {
+ ConfigGroups configGroups =
optionsClass.getAnnotation(ConfigGroups.class);
+ List<OptionWithMetaInfo> allOptions =
extractConfigOptions(optionsClass);
+ if (allOptions.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List<Tuple2<ConfigGroup, String>> tables;
+ if (configGroups != null) {
+ tables = new ArrayList<>(configGroups.groups().length + 1);
+ Tree tree = new Tree(configGroups.groups(), allOptions);
+ for (ConfigGroup group : configGroups.groups()) {
+ List<OptionWithMetaInfo> configOptions =
tree.findConfigOptions(group);
+ if (!configOptions.isEmpty()) {
+ sortOptions(configOptions);
+ tables.add(Tuple2.of(group, toHtmlTable(configOptions)));
+ }
+ }
+ List<OptionWithMetaInfo> configOptions = tree.getDefaultOptions();
+ if (!configOptions.isEmpty()) {
+ sortOptions(configOptions);
+ tables.add(Tuple2.of(null, toHtmlTable(configOptions)));
+ }
+ } else {
+ sortOptions(allOptions);
+ tables = Collections.singletonList(Tuple2.of(null,
toHtmlTable(allOptions)));
+ }
+ return tables;
+ }
+
+ @VisibleForTesting
+ static List<OptionWithMetaInfo> extractConfigOptions(Class<?> clazz) {
+ try {
+ List<OptionWithMetaInfo> configOptions = new ArrayList<>(8);
+ Field[] fields = clazz.getFields();
+ for (Field field : fields) {
+ if (isConfigOption(field) && shouldBeDocumented(field)) {
+ configOptions.add(
+ new OptionWithMetaInfo((ConfigOption<?>)
field.get(null), field));
+ }
+ }
+ return configOptions;
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to extract config options from class " + clazz +
'.', e);
+ }
+ }
+
+ private static boolean isConfigOption(Field field) {
+ return field.getType().equals(ConfigOption.class);
+ }
+
+ private static boolean shouldBeDocumented(Field field) {
+ return field.getAnnotation(Deprecated.class) == null
+ &&
field.getAnnotation(Documentation.ExcludeFromDocumentation.class) == null;
+ }
+
+ /**
+ * Transforms this configuration group into HTML formatted table. Options
are sorted
+ * alphabetically by key.
+ *
+ * @param options list of options to include in this group
+ * @return string containing HTML formatted table
+ */
+ private static String toHtmlTable(final List<OptionWithMetaInfo> options) {
+ StringBuilder htmlTable = new StringBuilder();
+ htmlTable.append("<table class=\"configuration table
table-bordered\">\n");
+ htmlTable.append(" <thead>\n");
+ htmlTable.append(" <tr>\n");
+ htmlTable.append(" <th class=\"text-left\" style=\"width:
20%\">Key</th>\n");
+ htmlTable.append(" <th class=\"text-left\" style=\"width:
15%\">Default</th>\n");
+ htmlTable.append(" <th class=\"text-left\" style=\"width:
10%\">Type</th>\n");
+ htmlTable.append(
+ " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n");
+ htmlTable.append(" </tr>\n");
+ htmlTable.append(" </thead>\n");
+ htmlTable.append(" <tbody>\n");
+ for (OptionWithMetaInfo option : options) {
+ htmlTable.append(toHtmlString(option));
+ }
+ htmlTable.append(" </tbody>\n");
+ htmlTable.append("</table>\n");
+ return htmlTable.toString();
+ }
+
+ /**
+ * Transforms option to table row.
+ *
+ * @param optionWithMetaInfo option to transform
+ * @return row with the option description
+ */
+ private static String toHtmlString(final OptionWithMetaInfo
optionWithMetaInfo) {
+ return ""
+ + " <tr>\n"
+ + " <td><h5>"
+ + escapeCharacters(optionWithMetaInfo.option.key())
+ + "</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">"
+ +
escapeCharacters(addWordBreakOpportunities(stringifyDefault(optionWithMetaInfo)))
+ + "</td>\n"
+ + " <td>"
+ + typeToHtml(optionWithMetaInfo)
+ + "</td>\n"
+ + " <td>"
+ + getDescription(optionWithMetaInfo)
+ + "</td>\n"
+ + " </tr>\n";
+ }
+
+ @VisibleForTesting
+ static String getDescription(OptionWithMetaInfo optionWithMetaInfo) {
+ return formatter.format(optionWithMetaInfo.option.description())
+ +
Optional.ofNullable(getEnumOptionsDescription(optionWithMetaInfo))
+ .map(formatter::format)
+ .map(desc -> String.format("<br /><br />%s", desc))
+ .orElse("");
+ }
+
+ /**
+ * Returns a {@link Description} for the enum constants of the given
option in case it is
+ * enum-based, and {@code null} otherwise.
+ */
+ private static @Nullable Description getEnumOptionsDescription(
+ OptionWithMetaInfo optionWithMetaInfo) {
+ Class<?> clazz = getClazz(optionWithMetaInfo.option);
+ if (!clazz.isEnum()) {
+ return null;
+ }
+ InlineElement[] optionDescriptions =
+ Arrays.stream(clazz.getEnumConstants())
+ .map(ConfigOptionsDocGenerator::formatEnumOption)
+ .map(elements -> TextElement.wrap(elements.toArray(new
InlineElement[0])))
+ .toArray(InlineElement[]::new);
+ return Description.builder().text("Possible
values:").list(optionDescriptions).build();
+ }
+
+ /**
+ * Formats a single enum constant.
+ *
+ * <p>If the enum implements {@link DescribedEnum}, this includes the
given description for each
+ * constant. Otherwise, only the constant itself is printed.
+ */
+ private static List<InlineElement> formatEnumOption(Object e) {
+ final List<InlineElement> elements = new LinkedList<>();
+ elements.add(text("\"%s\"", text(escapeCharacters(e.toString()))));
+
+ if (DescribedEnum.class.isAssignableFrom(e.getClass())) {
+ elements.add(text(": "));
+ elements.add(((DescribedEnum) e).getDescription());
+ }
+
+ return elements;
+ }
+
+ private static Class<?> getClazz(ConfigOption<?> option) {
+ try {
+ Method getClazzMethod =
ConfigOption.class.getDeclaredMethod("getClazz");
+ getClazzMethod.setAccessible(true);
+ Class<?> clazz = (Class<?>) getClazzMethod.invoke(option);
+ getClazzMethod.setAccessible(false);
+ return clazz;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static boolean isList(ConfigOption<?> option) {
+ try {
+ Method getClazzMethod =
ConfigOption.class.getDeclaredMethod("isList");
+ getClazzMethod.setAccessible(true);
+ boolean isList = (boolean) getClazzMethod.invoke(option);
+ getClazzMethod.setAccessible(false);
+ return isList;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @VisibleForTesting
+ static String typeToHtml(OptionWithMetaInfo optionWithMetaInfo) {
+ ConfigOption<?> option = optionWithMetaInfo.option;
+ Class<?> clazz = getClazz(option);
+ boolean isList = isList(option);
+ if (clazz.isEnum()) {
+ return enumTypeToHtml(isList);
+ }
+ return atomicTypeToHtml(clazz, isList);
+ }
+
+ private static String atomicTypeToHtml(Class<?> clazz, boolean isList) {
+ String typeName = clazz.getSimpleName();
+
+ final String type;
+ if (isList) {
+ type = String.format("List<%s>", typeName);
+ } else {
+ type = typeName;
+ }
+ return escapeCharacters(type);
+ }
+
+ private static String enumTypeToHtml(boolean isList) {
+ final String type;
+ if (isList) {
+ type = "List<Enum>";
+ } else {
+ type = "Enum";
+ }
+ return String.format("<p>%s</p>", escapeCharacters(type));
+ }
+
+ @VisibleForTesting
+ static String stringifyDefault(OptionWithMetaInfo optionWithMetaInfo) {
+ ConfigOption<?> option = optionWithMetaInfo.option;
+ Documentation.OverrideDefault overrideDocumentedDefault =
+
optionWithMetaInfo.field.getAnnotation(Documentation.OverrideDefault.class);
+ if (overrideDocumentedDefault != null) {
+ return overrideDocumentedDefault.value();
+ } else {
+ Object value = option.defaultValue();
+ return stringifyObject(value);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String stringifyObject(Object value) {
+ if (value instanceof String) {
+ if (((String) value).isEmpty()) {
+ return "(none)";
+ }
+ return "\"" + value + "\"";
+ } else if (value instanceof Duration) {
+ return TimeUtils.formatWithHighestUnit((Duration) value);
+ } else if (value instanceof List) {
+ return ((List<Object>) value)
+ .stream()
+ .map(ConfigOptionsDocGenerator::stringifyObject)
+ .collect(Collectors.joining(";"));
+ } else if (value instanceof Map) {
+ return ((Map<String, String>) value)
+ .entrySet().stream()
+ .map(e -> String.format("%s:%s", e.getKey(),
e.getValue()))
+ .collect(Collectors.joining(","));
+ }
+ return value == null ? "(none)" : value.toString();
+ }
+
+ private static String addWordBreakOpportunities(String value) {
+ return value
+ // allow breaking of semicolon separated lists
+ .replace(";", ";<wbr>");
+ }
+
+ private static void sortOptions(List<OptionWithMetaInfo> configOptions) {
+ configOptions.sort(Comparator.comparing(option ->
option.option.key()));
+ }
+
+ /**
+ * Data structure used to assign {@link ConfigOption ConfigOptions} to the
{@link ConfigGroup}
+ * with the longest matching prefix.
+ */
+ private static class Tree {
+ private final Node root = new Node();
+
+ Tree(ConfigGroup[] groups, Collection<OptionWithMetaInfo> options) {
+ // generate a tree based on all key prefixes
+ for (ConfigGroup group : groups) {
+ Node currentNode = root;
+ for (String keyComponent : group.keyPrefix().split("\\.")) {
+ currentNode = currentNode.addChild(keyComponent);
+ }
+ currentNode.markAsGroupRoot();
+ }
+
+ // assign options to their corresponding group, i.e. the last
group root node
+ // encountered when traversing
+ // the tree based on the option key
+ for (OptionWithMetaInfo option : options) {
+ findGroupRoot(option.option.key()).assignOption(option);
+ }
+ }
+
+ List<OptionWithMetaInfo> findConfigOptions(ConfigGroup configGroup) {
+ Node groupRoot = findGroupRoot(configGroup.keyPrefix());
+ return groupRoot.getConfigOptions();
+ }
+
+ List<OptionWithMetaInfo> getDefaultOptions() {
+ return root.getConfigOptions();
+ }
+
+ private Node findGroupRoot(String key) {
+ Node lastRootNode = root;
+ Node currentNode = root;
+ for (String keyComponent : key.split("\\.")) {
+ final Node childNode = currentNode.getChild(keyComponent);
+ if (childNode == null) {
+ break;
+ } else {
+ currentNode = childNode;
+ if (currentNode.isGroupRoot()) {
+ lastRootNode = currentNode;
+ }
+ }
+ }
+ return lastRootNode;
+ }
+
+ private static class Node {
+ private final List<OptionWithMetaInfo> configOptions = new
ArrayList<>(8);
+ private final Map<String, Node> children = new HashMap<>(8);
+ private boolean isGroupRoot = false;
+
+ private Node addChild(String keyComponent) {
+ Node child = children.get(keyComponent);
+ if (child == null) {
+ child = new Node();
+ children.put(keyComponent, child);
+ }
+ return child;
+ }
+
+ private Node getChild(String keyComponent) {
+ return children.get(keyComponent);
+ }
+
+ private void assignOption(OptionWithMetaInfo option) {
+ configOptions.add(option);
+ }
+
+ private boolean isGroupRoot() {
+ return isGroupRoot;
+ }
+
+ private void markAsGroupRoot() {
+ this.isGroupRoot = true;
+ }
+
+ private List<OptionWithMetaInfo> getConfigOptions() {
+ return configOptions;
+ }
+ }
+ }
+
+ static class OptionWithMetaInfo {
+ final ConfigOption<?> option;
+ final Field field;
+
+ public OptionWithMetaInfo(ConfigOption<?> option, Field field) {
+ this.option = option;
+ this.field = field;
+ }
+ }
+
+ private ConfigOptionsDocGenerator() {}
+}
diff --git
a/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/OptionsClassLocation.java
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/OptionsClassLocation.java
new file mode 100644
index 00000000..5a6df0ff
--- /dev/null
+++
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/configuration/OptionsClassLocation.java
@@ -0,0 +1,46 @@
+/*
+ * 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.flink.table.store.docs.configuration;
+
+import org.apache.flink.configuration.ConfigOption;
+
+/** Simple descriptor for the location of a class containing {@link
ConfigOption ConfigOptions}. */
+class OptionsClassLocation {
+
+ private final String module;
+ private final String pckg;
+
+ OptionsClassLocation(String module, String pckg) {
+ this.module = module;
+ this.pckg = pckg;
+ }
+
+ public String getModule() {
+ return module;
+ }
+
+ public String getPackage() {
+ return pckg;
+ }
+
+ @Override
+ public String toString() {
+ return module + "#" + pckg;
+ }
+}
diff --git
a/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/util/Utils.java
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/util/Utils.java
new file mode 100644
index 00000000..9d022b7f
--- /dev/null
+++
b/flink-table-store-docs/src/main/java/org/apache/flink/table/store/docs/util/Utils.java
@@ -0,0 +1,36 @@
+/*
+ * 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.flink.table.store.docs.util;
+
+/** Contains various shared utility functions. */
+public class Utils {
+
+ /**
+ * Placeholder that is used to prevent certain sections from being
escaped. We don't need a
+ * sophisticated value but only something that won't show up in config
options.
+ */
+ private static final String TEMPORARY_PLACEHOLDER =
"superRandomTemporaryPlaceholder";
+
+ public static String escapeCharacters(String value) {
+ return value.replaceAll("<wbr>", TEMPORARY_PLACEHOLDER)
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll(TEMPORARY_PLACEHOLDER, "<wbr>");
+ }
+}
diff --git a/flink-table-store-docs/src/main/resources/log4j.properties
b/flink-table-store-docs/src/main/resources/log4j.properties
new file mode 100644
index 00000000..45588487
--- /dev/null
+++ b/flink-table-store-docs/src/main/resources/log4j.properties
@@ -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.
+################################################################################
+
+log4j.logger.org.apache.flink.table.store.docs.ConfigOptionsDocGenerator=
INFO, logger
+
+log4j.appender.logger=org.apache.log4j.ConsoleAppender
+log4j.appender.logger.target = System.out
+log4j.appender.logger.layout=org.apache.log4j.PatternLayout
+log4j.appender.logger.layout.ConversionPattern=%-5p - %m%n
diff --git
a/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGeneratorTest.java
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGeneratorTest.java
new file mode 100644
index 00000000..c6be5575
--- /dev/null
+++
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocGeneratorTest.java
@@ -0,0 +1,608 @@
+/*
+ * 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.flink.table.store.docs.configuration;
+
+import org.apache.flink.annotation.docs.ConfigGroup;
+import org.apache.flink.annotation.docs.ConfigGroups;
+import org.apache.flink.annotation.docs.Documentation;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.ConfigOption;
+import org.apache.flink.configuration.ConfigOptions;
+import org.apache.flink.configuration.DescribedEnum;
+import org.apache.flink.configuration.MemorySize;
+import org.apache.flink.configuration.description.Formatter;
+import org.apache.flink.configuration.description.HtmlFormatter;
+import org.apache.flink.configuration.description.InlineElement;
+import org.apache.flink.table.store.docs.configuration.data.TestCommonOptions;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.flink.configuration.description.TextElement.text;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for the {@link ConfigOptionsDocGenerator}. */
+@SuppressWarnings("unused")
+class ConfigOptionsDocGeneratorTest {
+
+ static class TestConfigGroup {
+ public static ConfigOption<Integer> firstOption =
+ ConfigOptions.key("first.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is example description for the
first option.");
+
+ public static ConfigOption<String> secondOption =
+ ConfigOptions.key("second.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This is long example description for
the second option.");
+ }
+
+ private enum TestEnum {
+ VALUE_1,
+ VALUE_2,
+ VALUE_3
+ }
+
+ private enum DescribedTestEnum implements DescribedEnum {
+ A(text("First letter of the alphabet")),
+ B(text("Second letter of the alphabet"));
+
+ private final InlineElement description;
+
+ DescribedTestEnum(InlineElement description) {
+ this.description = description;
+ }
+
+ @Override
+ public InlineElement getDescription() {
+ return description;
+ }
+ }
+
+ static class TypeTestConfigGroup {
+ public static ConfigOption<TestEnum> enumOption =
+ ConfigOptions.key("option.enum")
+ .enumType(TestEnum.class)
+ .defaultValue(TestEnum.VALUE_1)
+ .withDescription("Description");
+
+ public static ConfigOption<List<TestEnum>> enumListOption =
+ ConfigOptions.key("option.enum.list")
+ .enumType(TestEnum.class)
+ .asList()
+ .defaultValues(TestEnum.VALUE_1, TestEnum.VALUE_2)
+ .withDescription("Description");
+
+ public static ConfigOption<DescribedTestEnum> describedEnum =
+ ConfigOptions.key("option.enum.described")
+ .enumType(DescribedTestEnum.class)
+ .noDefaultValue()
+ .withDescription("Description");
+
+ public static ConfigOption<MemorySize> memoryOption =
+ ConfigOptions.key("option.memory")
+ .memoryType()
+ .defaultValue(new MemorySize(1024))
+ .withDescription("Description");
+
+ public static ConfigOption<Map<String, String>> mapOption =
+ ConfigOptions.key("option.map")
+ .mapType()
+ .defaultValue(Collections.singletonMap("key1",
"value1"))
+ .withDescription("Description");
+
+ public static ConfigOption<List<Map<String, String>>> mapListOption =
+ ConfigOptions.key("option.map.list")
+ .mapType()
+ .asList()
+ .defaultValues(
+ Collections.singletonMap("key1", "value1"),
+ Collections.singletonMap("key2", "value2"))
+ .withDescription("Description");
+
+ public static ConfigOption<Duration> durationOption =
+ ConfigOptions.key("option.duration")
+ .durationType()
+ .defaultValue(Duration.ofMinutes(1))
+ .withDescription("Description");
+ }
+
+ @Test
+ void testCreatingTypes() {
+ final String expectedTable =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>option.duration</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">1
min</td>\n"
+ + " <td>Duration</td>\n"
+ + " <td>Description</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>option.enum</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">VALUE_1</td>\n"
+ + " <td><p>Enum</p></td>\n"
+ + " <td>Description<br /><br />Possible
values:<ul><li>\"VALUE_1\"</li><li>\"VALUE_2\"</li><li>\"VALUE_3\"</li></ul></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + "
<td><h5>option.enum.described</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">(none)</td>\n"
+ + " <td><p>Enum</p></td>\n"
+ + " <td>Description<br /><br />Possible
values:<ul><li>\"A\": First letter of the alphabet</li><li>\"B\": Second letter
of the alphabet</li></ul></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>option.enum.list</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">VALUE_1;<wbr>VALUE_2</td>\n"
+ + " <td><p>List<Enum></p></td>\n"
+ + " <td>Description<br /><br />Possible
values:<ul><li>\"VALUE_1\"</li><li>\"VALUE_2\"</li><li>\"VALUE_3\"</li></ul></td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>option.map</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">key1:value1</td>\n"
+ + " <td>Map</td>\n"
+ + " <td>Description</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>option.map.list</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">key1:value1;<wbr>key2:value2</td>\n"
+ + " <td>List<Map></td>\n"
+ + " <td>Description</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>option.memory</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">1
kb</td>\n"
+ + " <td>MemorySize</td>\n"
+ + " <td>Description</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ final String htmlTable =
+
ConfigOptionsDocGenerator.generateTablesForClass(TypeTestConfigGroup.class)
+ .get(0)
+ .f1;
+ assertThat(htmlTable).isEqualTo(expectedTable);
+ }
+
+ @Test
+ void testCreatingDescription() {
+ final String expectedTable =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>first.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">2</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>This is example description for the
first option.</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>second.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">(none)</td>\n"
+ + " <td>String</td>\n"
+ + " <td>This is long example description
for the second option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ final String htmlTable =
+
ConfigOptionsDocGenerator.generateTablesForClass(TestConfigGroup.class).get(0).f1;
+ assertThat(htmlTable).isEqualTo(expectedTable);
+ }
+
+ @ConfigGroups(
+ groups = {
+ @ConfigGroup(name = "group1", keyPrefix = "a.b"),
+ @ConfigGroup(name = "group2", keyPrefix = "a.b.c.d")
+ })
+ static class TestConfigPrefix {
+ // should end up in the default group
+ public static ConfigOption<Integer> option1 =
+ ConfigOptions.key("a.option").intType().defaultValue(2);
+
+ // should end up in group1, perfect key-prefix match
+ public static ConfigOption<String> option2 =
+ ConfigOptions.key("a.b.option").stringType().noDefaultValue();
+
+ // should end up in group1, full key-prefix match
+ public static ConfigOption<Integer> option3 =
+ ConfigOptions.key("a.b.c.option").intType().defaultValue(2);
+
+ // should end up in group1, full key-prefix match for group 1, partial
match for group 2
+ // checks that the generator remembers the last encountered root node
+ public static ConfigOption<Integer> option4 =
+ ConfigOptions.key("a.b.c.e.option").intType().defaultValue(2);
+
+ // should end up in the default group, since no group exists with
prefix "a.c"
+ // checks that the generator does not ignore components (like ignoring
"c" to find a match
+ // "a.b")
+ public static ConfigOption<String> option5 =
+
ConfigOptions.key("a.c.b.option").stringType().noDefaultValue();
+
+ // should end up in group2, full key-prefix match for group 2
+ // checks that the longest matching group is assigned
+ public static ConfigOption<Integer> option6 =
+ ConfigOptions.key("a.b.c.d.option").intType().defaultValue(2);
+ }
+
+ @Test
+ void testLongestPrefixMatching() {
+ final List<Tuple2<ConfigGroup, String>> tables =
+
ConfigOptionsDocGenerator.generateTablesForClass(TestConfigPrefix.class);
+ assertThat(tables).hasSize(3);
+ final Map<String, String> tablesConverted = new
HashMap<>(tables.size());
+ for (final Tuple2<ConfigGroup, String> table : tables) {
+ tablesConverted.put(table.f0 != null ? table.f0.name() :
"default", table.f1);
+ }
+ assertThat(tablesConverted.get("group1"))
+ .contains("a.b.option", "a.b.c.option", "a.b.c.e.option")
+ .doesNotContain("a.b.c.d.option");
+ assertThat(tablesConverted.get("group2")).contains("a.b.c.d.option");
+ assertThat(tablesConverted.get("default")).contains("a.option",
"a.c.b.option");
+ }
+
+ @ConfigGroups(
+ groups = {
+ @ConfigGroup(name = "firstGroup", keyPrefix = "first"),
+ @ConfigGroup(name = "secondGroup", keyPrefix = "second")
+ })
+ static class TestConfigMultipleSubGroup {
+ public static ConfigOption<Integer> firstOption =
+ ConfigOptions.key("first.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is example description for the
first option.");
+
+ public static ConfigOption<String> secondOption =
+ ConfigOptions.key("second.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This is long example description for
the second option.");
+
+ public static ConfigOption<Integer> thirdOption =
+ ConfigOptions.key("third.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is example description for the
third option.");
+
+ public static ConfigOption<String> fourthOption =
+ ConfigOptions.key("fourth.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This is long example description for
the fourth option.");
+ }
+
+ @Test
+ void testCreatingMultipleGroups() {
+ final List<Tuple2<ConfigGroup, String>> tables =
+
ConfigOptionsDocGenerator.generateTablesForClass(TestConfigMultipleSubGroup.class);
+ assertThat(tables).hasSize(3);
+ final HashMap<String, String> tablesConverted = new HashMap<>();
+ for (Tuple2<ConfigGroup, String> table : tables) {
+ tablesConverted.put(table.f0 != null ? table.f0.name() :
"default", table.f1);
+ }
+ assertThat(tablesConverted)
+ .containsEntry(
+ "firstGroup",
+ "<table class=\"configuration table
table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\"
style=\"width: 20%\">Key</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 15%\">Default</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 10%\">Type</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + "
<td><h5>first.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">2</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>This is example description
for the first option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n");
+ assertThat(tablesConverted)
+ .containsEntry(
+ "secondGroup",
+ "<table class=\"configuration table
table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\"
style=\"width: 20%\">Key</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 15%\">Default</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 10%\">Type</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + "
<td><h5>second.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">(none)</td>\n"
+ + " <td>String</td>\n"
+ + " <td>This is long example
description for the second option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n");
+ assertThat(tablesConverted)
+ .containsEntry(
+ "default",
+ "<table class=\"configuration table
table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\"
style=\"width: 20%\">Key</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 15%\">Default</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 10%\">Type</th>\n"
+ + " <th class=\"text-left\"
style=\"width: 55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + "
<td><h5>fourth.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">(none)</td>\n"
+ + " <td>String</td>\n"
+ + " <td>This is long example
description for the fourth option.</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + "
<td><h5>third.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">2</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>This is example description
for the third option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n");
+ }
+
+ static class TestConfigGroupWithOverriddenDefault {
+ @Documentation.OverrideDefault("default_1")
+ public static ConfigOption<Integer> firstOption =
+ ConfigOptions.key("first.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is example description for the
first option.");
+
+ @Documentation.OverrideDefault("default_2")
+ public static ConfigOption<String> secondOption =
+ ConfigOptions.key("second.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This is long example description for
the second option.");
+ }
+
+ @Test
+ void testOverrideDefault() {
+ final String expectedTable =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>first.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">default_1</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>This is example description for the
first option.</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>second.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">default_2</td>\n"
+ + " <td>String</td>\n"
+ + " <td>This is long example description
for the second option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ final String htmlTable =
+ ConfigOptionsDocGenerator.generateTablesForClass(
+ TestConfigGroupWithOverriddenDefault.class)
+ .get(0)
+ .f1;
+ assertThat(htmlTable).isEqualTo(expectedTable);
+ }
+
+ @Test
+ void testSections(@TempDir File tmpDir) throws IOException,
ClassNotFoundException {
+ final String projectRootDir = getProjectRootDir();
+ final String outputDirectory = tmpDir.getAbsolutePath();
+ final OptionsClassLocation[] locations =
+ new OptionsClassLocation[] {
+ new OptionsClassLocation(
+ "flink-table-store-docs",
+ TestCommonOptions.class.getPackage().getName())
+ };
+ ConfigOptionsDocGenerator.generateCommonSection(
+ projectRootDir, outputDirectory, locations, "src/test/java");
+ Formatter formatter = new HtmlFormatter();
+ String expected1 =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>"
+ + TestCommonOptions.COMMON_POSITIONED_OPTION.key()
+ + "</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">"
+ +
TestCommonOptions.COMMON_POSITIONED_OPTION.defaultValue()
+ + "</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>"
+ +
formatter.format(TestCommonOptions.COMMON_POSITIONED_OPTION.description())
+ + "</td>\n"
+ + " </tr>\n"
+ + " <tr>\n"
+ + " <td><h5>"
+ + TestCommonOptions.COMMON_OPTION.key()
+ + "</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">"
+ + TestCommonOptions.COMMON_OPTION.defaultValue()
+ + "</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>"
+ +
formatter.format(TestCommonOptions.COMMON_OPTION.description())
+ + "</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ String expected2 =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>"
+ + TestCommonOptions.COMMON_OPTION.key()
+ + "</h5></td>\n"
+ + " <td style=\"word-wrap: break-word;\">"
+ + TestCommonOptions.COMMON_OPTION.defaultValue()
+ + "</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>"
+ +
formatter.format(TestCommonOptions.COMMON_OPTION.description())
+ + "</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ final String fileName1 =
+
ConfigOptionsDocGenerator.getSectionFileName(TestCommonOptions.SECTION_1);
+ final String fileName2 =
+
ConfigOptionsDocGenerator.getSectionFileName(TestCommonOptions.SECTION_2);
+ assertThat(new File(outputDirectory, fileName1))
+ .content(StandardCharsets.UTF_8)
+ .isEqualTo(expected1);
+ assertThat(new File(outputDirectory, fileName2))
+ .content(StandardCharsets.UTF_8)
+ .isEqualTo(expected2);
+ }
+
+ static class TestConfigGroupWithExclusion {
+ public static ConfigOption<Integer> firstOption =
+ ConfigOptions.key("first.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is example description for the
first option.");
+
+ @Documentation.ExcludeFromDocumentation
+ public static ConfigOption<String> excludedOption =
+ ConfigOptions.key("excluded.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This should not be documented.");
+ }
+
+ /**
+ * Tests that {@link ConfigOption} annotated with {@link
Documentation.ExcludeFromDocumentation}
+ * are not documented.
+ */
+ @Test
+ void testConfigOptionExclusion() {
+ final String expectedTable =
+ "<table class=\"configuration table table-bordered\">\n"
+ + " <thead>\n"
+ + " <tr>\n"
+ + " <th class=\"text-left\" style=\"width:
20%\">Key</th>\n"
+ + " <th class=\"text-left\" style=\"width:
15%\">Default</th>\n"
+ + " <th class=\"text-left\" style=\"width:
10%\">Type</th>\n"
+ + " <th class=\"text-left\" style=\"width:
55%\">Description</th>\n"
+ + " </tr>\n"
+ + " </thead>\n"
+ + " <tbody>\n"
+ + " <tr>\n"
+ + " <td><h5>first.option.a</h5></td>\n"
+ + " <td style=\"word-wrap:
break-word;\">2</td>\n"
+ + " <td>Integer</td>\n"
+ + " <td>This is example description for the
first option.</td>\n"
+ + " </tr>\n"
+ + " </tbody>\n"
+ + "</table>\n";
+ final String htmlTable =
+
ConfigOptionsDocGenerator.generateTablesForClass(TestConfigGroupWithExclusion.class)
+ .get(0)
+ .f1;
+ assertThat(htmlTable).isEqualTo(expectedTable);
+ }
+
+ @ConfigGroups(groups = {@ConfigGroup(name = "firstGroup", keyPrefix =
"first")})
+ static class EmptyConfigOptions {}
+
+ @Test
+ void testClassWithoutOptionsIsIgnored() {
+
assertThat(ConfigOptionsDocGenerator.generateTablesForClass(EmptyConfigOptions.class))
+ .isEmpty();
+ }
+
+ @Test
+ void testSnakeCaseConversion() {
+ assertThat(ConfigOptionsDocGenerator.toSnakeCase("RocksOptions"))
+ .isEqualTo("rocks_options");
+ assertThat(ConfigOptionsDocGenerator.toSnakeCase("RocksDBOptions"))
+ .isEqualTo("rocksdb_options");
+
assertThat(ConfigOptionsDocGenerator.toSnakeCase("DBOptions")).isEqualTo("db_options");
+ }
+
+ static String getProjectRootDir() {
+ final String dirFromProperty = System.getProperty("rootDir");
+ if (dirFromProperty != null) {
+ return dirFromProperty;
+ }
+
+ // to make this work in the IDE use a default fallback
+ final String currentDir = System.getProperty("user.dir");
+ return new File(currentDir).getParent();
+ }
+}
diff --git
a/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocsCompletenessITCase.java
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocsCompletenessITCase.java
new file mode 100644
index 00000000..7697e106
--- /dev/null
+++
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/ConfigOptionsDocsCompletenessITCase.java
@@ -0,0 +1,377 @@
+/*
+ * 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.flink.table.store.docs.configuration;
+
+import org.apache.flink.annotation.docs.Documentation;
+import org.apache.flink.api.java.tuple.Tuple2;
+import org.apache.flink.configuration.ConfigOption;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.DEFAULT_PATH_PREFIX;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.LOCATIONS;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.extractConfigOptions;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.getDescription;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.processConfigOptions;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.stringifyDefault;
+import static
org.apache.flink.table.store.docs.configuration.ConfigOptionsDocGenerator.typeToHtml;
+import static org.assertj.core.api.Fail.fail;
+
+/**
+ * This test verifies that all {@link ConfigOption ConfigOptions} in the
configured {@link
+ * ConfigOptionsDocGenerator#LOCATIONS locations} are documented and
well-defined (i.e. no 2 options
+ * exist for the same key with different descriptions/default values), and
that the documentation
+ * does not refer to non-existent options.
+ */
+public class ConfigOptionsDocsCompletenessITCase {
+
+ @Test
+ public void testCompleteness() throws IOException, ClassNotFoundException {
+ compareDocumentedAndExistingOptions(
+ parseDocumentedOptions(),
+ checkWellDefinedAndDeduplicate(findExistingOptions(ignored ->
true)));
+ }
+
+ private static Map<String, List<ExistingOption>>
checkWellDefinedAndDeduplicate(
+ Map<String, List<ExistingOption>> allOptions) {
+ return allOptions.entrySet().stream()
+ .map(
+ (entry) -> {
+ final List<ExistingOption> existingOptions =
entry.getValue();
+ final List<ExistingOption> consolidated;
+ if (existingOptions.stream()
+ .allMatch(option ->
option.isSuffixOption)) {
+ consolidated = existingOptions;
+ } else {
+ consolidated =
+ Collections.singletonList(
+ existingOptions.stream()
+ .reduce(
+ (option1,
option2) -> {
+ if
(option1.equals(option2)) {
+ // we
allow multiple
+ //
instances of
+ //
ConfigOptions with the
+ //
same key
+ // if
they are identical
+ return
option1;
+ } else {
+ //
found a ConfigOption pair
+ // with
+ // the
same key that aren't
+ //
equal
+ // we
fail here outright as
+ //
this is
+ // not
a
+ //
documentation-completeness
+ //
problem
+ String
errorMessage;
+ if
(!option1.defaultValue
+
.equals(
+
option2.defaultValue)) {
+
errorMessage =
+
String.format(
+
"Ambiguous option %s due to distinct default values (%s (in %s) vs
%s (in %s)).",
+
option1.key,
+
option1.defaultValue,
+
option1
+
.containingClass
+
.getSimpleName(),
+
option2.defaultValue,
+
option2
+
.containingClass
+
.getSimpleName());
+ } else
{
+
errorMessage =
+
String.format(
+
"Ambiguous option %s due to distinct descriptions (%s vs %s).",
+
option1.key,
+
option1
+
.containingClass
+
.getSimpleName(),
+
option2
+
.containingClass
+
.getSimpleName());
+ }
+ throw
new AssertionError(
+
errorMessage);
+ }
+ })
+ .get());
+ }
+ return new Tuple2<>(entry.getKey(), consolidated);
+ })
+ .collect(Collectors.toMap((t) -> t.f0, (t) -> t.f1));
+ }
+
+ private static void compareDocumentedAndExistingOptions(
+ Map<String, List<DocumentedOption>> documentedOptions,
+ Map<String, List<ExistingOption>> existingOptions) {
+ final Collection<String> problems = new ArrayList<>(0);
+ // first check that all existing options are properly documented
+ existingOptions.forEach(
+ (key, supposedStates) -> {
+ List<DocumentedOption> documentedState =
documentedOptions.get(key);
+ for (ExistingOption supposedState : supposedStates) {
+ if (documentedState == null ||
documentedState.isEmpty()) {
+ // option is not documented at all
+ problems.add(
+ "Option "
+ + supposedState.key
+ + " in "
+ + supposedState.containingClass
+ + " is not documented.");
+ } else {
+ final Iterator<DocumentedOption> candidates =
+ documentedState.iterator();
+ boolean matchFound = false;
+ while (candidates.hasNext()) {
+ DocumentedOption candidate = candidates.next();
+ if
(supposedState.defaultValue.equals(candidate.defaultValue)
+ && supposedState.description.equals(
+ candidate.description)) {
+ matchFound = true;
+ candidates.remove();
+ }
+ }
+ if (documentedState.isEmpty()) {
+ documentedOptions.remove(key);
+ }
+ if (!matchFound) {
+ problems.add(
+ String.format(
+ "Documentation of %s in %s is
outdated. Expected: default=(%s) description=(%s).",
+ supposedState.key,
+
supposedState.containingClass.getSimpleName(),
+ supposedState.defaultValue,
+ supposedState.description));
+ }
+ }
+ }
+ });
+
+ // documentation contains an option that no longer exists
+ documentedOptions.values().stream()
+ .flatMap(Collection::stream)
+ .forEach(
+ documentedOption ->
+ problems.add(
+ "Documented option "
+ + documentedOption.key
+ + " does not exist."));
+ if (!problems.isEmpty()) {
+ StringBuilder stringBuilder =
+ new StringBuilder(
+ "Documentation is outdated, please regenerate it
according to the"
+ + " instructions in
flink-table-store-docs/README.md.");
+ stringBuilder.append(System.lineSeparator());
+ stringBuilder.append("\tProblems:");
+ for (String problem : problems) {
+ stringBuilder.append(System.lineSeparator());
+ stringBuilder.append("\t\t");
+ stringBuilder.append(problem);
+ }
+ fail(stringBuilder.toString());
+ }
+ }
+
+ private static Map<String, List<DocumentedOption>>
parseDocumentedOptions() throws IOException {
+ final String rootDir =
ConfigOptionsDocGeneratorTest.getProjectRootDir();
+ Path includeFolder =
+ Paths.get(rootDir, "docs", "layouts", "shortcodes",
"generated").toAbsolutePath();
+ return Files.list(includeFolder)
+ .filter(
+ (path) -> {
+ final String filename =
path.getFileName().toString();
+ return filename.endsWith("configuration.html");
+ })
+ .flatMap(
+ file -> {
+ try {
+ return
parseDocumentedOptionsFromFile(file).stream();
+ } catch (IOException ignored) {
+ return Stream.empty();
+ }
+ })
+ .collect(Collectors.groupingBy(option -> option.key,
Collectors.toList()));
+ }
+
+ private static Collection<DocumentedOption>
parseDocumentedOptionsFromFile(Path file)
+ throws IOException {
+ Document document = Jsoup.parse(file.toFile(),
StandardCharsets.UTF_8.name());
+ document.outputSettings().syntax(Document.OutputSettings.Syntax.xml);
+ document.outputSettings().prettyPrint(false);
+ return document.getElementsByTag("table").stream()
+ .map(element -> element.getElementsByTag("tbody").get(0))
+ .flatMap(element -> element.getElementsByTag("tr").stream())
+ .map(
+ tableRow -> {
+ // Use split to exclude document key tag.
+ String key = tableRow.child(0).text().split("
")[0];
+ String defaultValue = tableRow.child(1).text();
+ String typeValue = tableRow.child(2).html();
+ String description =
+ tableRow.child(3).childNodes().stream()
+ .map(Object::toString)
+ .collect(Collectors.joining());
+ return new DocumentedOption(key, defaultValue,
typeValue, description);
+ })
+ .collect(Collectors.toList());
+ }
+
+ private static Map<String, List<ExistingOption>> findExistingOptions(
+ Predicate<ConfigOptionsDocGenerator.OptionWithMetaInfo> predicate)
+ throws IOException, ClassNotFoundException {
+ final String rootDir =
ConfigOptionsDocGeneratorTest.getProjectRootDir();
+ final Collection<ExistingOption> existingOptions = new ArrayList<>();
+ for (OptionsClassLocation location : LOCATIONS) {
+ processConfigOptions(
+ rootDir,
+ location.getModule(),
+ location.getPackage(),
+ DEFAULT_PATH_PREFIX,
+ optionsClass ->
+ extractConfigOptions(optionsClass).stream()
+ .filter(predicate)
+ .map(
+ optionWithMetaInfo ->
+ toExistingOption(
+
optionWithMetaInfo, optionsClass))
+ .forEach(existingOptions::add));
+ }
+ return existingOptions.stream()
+ .collect(Collectors.groupingBy(option -> option.key,
Collectors.toList()));
+ }
+
+ private static ExistingOption toExistingOption(
+ ConfigOptionsDocGenerator.OptionWithMetaInfo optionWithMetaInfo,
+ Class<?> optionsClass) {
+ String key = optionWithMetaInfo.option.key();
+ String defaultValue = stringifyDefault(optionWithMetaInfo);
+ String typeValue = typeToHtml(optionWithMetaInfo);
+ String description = getDescription(optionWithMetaInfo);
+ boolean isSuffixOption = isSuffixOption(optionWithMetaInfo.field);
+ return new ExistingOption(
+ key, defaultValue, typeValue, description, optionsClass,
isSuffixOption);
+ }
+
+ private static boolean isSuffixOption(Field field) {
+ final Class<?> containingOptionsClass = field.getDeclaringClass();
+
+ return field.getAnnotation(Documentation.SuffixOption.class) != null
+ ||
containingOptionsClass.getAnnotation(Documentation.SuffixOption.class) != null;
+ }
+
+ private static final class ExistingOption extends Option {
+
+ private final Class<?> containingClass;
+ private final boolean isSuffixOption;
+
+ private ExistingOption(
+ String key,
+ String defaultValue,
+ String typeValue,
+ String description,
+ Class<?> containingClass,
+ boolean isSuffixOption) {
+ super(key, defaultValue, typeValue, description);
+ this.containingClass = containingClass;
+ this.isSuffixOption = isSuffixOption;
+ }
+ }
+
+ private static final class DocumentedOption extends Option {
+
+ private DocumentedOption(
+ String key, String defaultValue, String typeValue, String
description) {
+ super(key, defaultValue, typeValue, description);
+ }
+ }
+
+ private abstract static class Option {
+ protected final String key;
+ protected final String defaultValue;
+ protected final String typeValue;
+ protected final String description;
+
+ private Option(String key, String defaultValue, String typeValue,
String description) {
+ this.key = key;
+ this.defaultValue = defaultValue;
+ this.typeValue = typeValue;
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Option option = (Option) o;
+ return Objects.equals(key, option.key)
+ && Objects.equals(defaultValue, option.defaultValue)
+ && Objects.equals(typeValue, option.typeValue)
+ && Objects.equals(description, option.description);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, defaultValue, typeValue, description);
+ }
+
+ @Override
+ public String toString() {
+ return "Option{"
+ + "key='"
+ + key
+ + '\''
+ + ", defaultValue='"
+ + defaultValue
+ + '\''
+ + ", typeValue='"
+ + typeValue
+ + '\''
+ + ", description='"
+ + description
+ + '\''
+ + '}';
+ }
+ }
+}
diff --git
a/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/data/TestCommonOptions.java
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/data/TestCommonOptions.java
new file mode 100644
index 00000000..79443cc4
--- /dev/null
+++
b/flink-table-store-docs/src/test/java/org/apache/flink/table/store/docs/configuration/data/TestCommonOptions.java
@@ -0,0 +1,51 @@
+/*
+ * 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.flink.table.store.docs.configuration.data;
+
+import org.apache.flink.annotation.docs.Documentation;
+import org.apache.flink.configuration.ConfigOption;
+import org.apache.flink.configuration.ConfigOptions;
+
+/** Collection of test {@link ConfigOptions ConfigOptions}. */
+@SuppressWarnings("unused") // this class is only accessed reflectively
+public class TestCommonOptions {
+
+ public static final String SECTION_1 = "test_A";
+ public static final String SECTION_2 = "other";
+
+ @Documentation.Section({SECTION_1, SECTION_2})
+ public static final ConfigOption<Integer> COMMON_OPTION =
+ ConfigOptions.key("first.option.a")
+ .intType()
+ .defaultValue(2)
+ .withDescription("This is the description for the common
option.");
+
+ public static final ConfigOption<String> GENERIC_OPTION =
+ ConfigOptions.key("second.option.a")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("This is the description for the generic
option.");
+
+ @Documentation.Section(value = SECTION_1, position = 2)
+ public static final ConfigOption<Integer> COMMON_POSITIONED_OPTION =
+ ConfigOptions.key("third.option.a")
+ .intType()
+ .defaultValue(3)
+ .withDescription("This is the description for the
positioned common option.");
+}
diff --git a/pom.xml b/pom.xml
index 2e543280..b2408b37 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,7 @@ under the License.
<module>flink-table-store-connector</module>
<module>flink-table-store-core</module>
<module>flink-table-store-dist</module>
+ <module>flink-table-store-docs</module>
<module>flink-table-store-e2e-tests</module>
<module>flink-table-store-format</module>
<module>flink-table-store-shade</module>
@@ -395,6 +396,7 @@ under the License.
<exclude>docs/themes/book/**</exclude>
<exclude>docs/assets/github.css</exclude>
<exclude>docs/static/js/anchor.min.js</exclude>
+ <exclude>docs/layouts/shortcodes/generated/**</exclude>
<exclude>**/*.svg</exclude>
<!-- Bundled license files -->
<exclude>**/LICENSE*</exclude>
@@ -641,6 +643,29 @@ under the License.
</execution>
</executions>
</plugin>
+
+ <!-- generate configuration docs -->
+ <plugin>
+ <groupId>org.commonjava.maven.plugins</groupId>
+ <artifactId>directory-maven-plugin</artifactId>
+ <version>0.1</version>
+ <executions>
+ <execution>
+ <id>directories</id>
+ <goals>
+ <goal>directory-of</goal>
+ </goals>
+ <phase>initialize</phase>
+ <configuration>
+ <property>rootDir</property>
+ <project>
+ <groupId>org.apache.flink</groupId>
+
<artifactId>flink-table-store-parent</artifactId>
+ </project>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
<pluginManagement>