You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@seatunnel.apache.org by ga...@apache.org on 2023/05/10 20:22:20 UTC

[incubator-seatunnel-web] branch add_canvas_job_define updated: [Feature][DynamicForm] Add seatunnel dynamicform module (#49)

This is an automated email from the ASF dual-hosted git repository.

gaojun2048 pushed a commit to branch add_canvas_job_define
in repository https://gitbox.apache.org/repos/asf/incubator-seatunnel-web.git


The following commit(s) were added to refs/heads/add_canvas_job_define by this push:
     new fa2e3333 [Feature][DynamicForm] Add seatunnel dynamicform module (#49)
fa2e3333 is described below

commit fa2e3333d4fe338204a58d2d8b565151e8f126dc
Author: Eric <ga...@gmail.com>
AuthorDate: Thu May 11 04:22:15 2023 +0800

    [Feature][DynamicForm] Add seatunnel dynamicform module (#49)
---
 .github/workflows/backend.yml                      |   2 +-
 pom.xml                                            | 148 ++++++++-
 seatunnel-server/pom.xml                           |   1 +
 seatunnel-server/seatunnel-app/pom.xml             |   4 +
 .../src/main/resources/application.yml             |   2 +-
 .../app/controller/UserControllerTest.java         |   9 +-
 .../pom.xml                                        |  42 ++-
 .../app/dynamicforms/AbstractFormOption.java       | 118 ++++++++
 .../app/dynamicforms/AbstractFormSelectOption.java |  49 +++
 .../seatunnel/app/dynamicforms/Constants.java      |  25 ++
 .../app/dynamicforms/DynamicSelectOption.java      |  33 +++
 .../app/dynamicforms/FormInputOption.java          |  56 ++++
 .../seatunnel/app/dynamicforms/FormLocale.java     |  51 ++++
 .../app/dynamicforms/FormOptionBuilder.java        | 165 +++++++++++
 .../seatunnel/app/dynamicforms/FormStructure.java  |  65 ++++
 .../app/dynamicforms/FormStructureBuilder.java     |  74 +++++
 .../app/dynamicforms/FormStructureValidate.java    | 273 +++++++++++++++++
 .../app/dynamicforms/StaticSelectOption.java       |  38 +++
 .../exception/FormStructureValidateException.java  |  35 +++
 .../dynamicforms/validate/AbstractValidate.java    |  65 ++++
 .../validate/MutuallyExclusiveValidate.java        |  42 +++
 .../dynamicforms/validate/NonEmptyValidate.java    |  30 ++
 .../validate/UnionNonEmptyValidate.java            |  45 +++
 .../app/dynamicforms/validate/ValidateBuilder.java |  78 +++++
 .../app/dynamicforms/FormStructureBuilderTest.java | 329 +++++++++++++++++++++
 .../seatunnel/app/dynamicforms/TestUtils.java      |  34 +++
 .../src/test/resources/test_form.json              |   1 +
 seatunnel-server/seatunnel-server-common/pom.xml   |   7 +-
 seatunnel-ui/src/router/routes.ts                  |   2 +-
 tools/dependencies/known-dependencies.txt          |   7 +-
 30 files changed, 1805 insertions(+), 25 deletions(-)

diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index efc139b1..eea3507e 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -125,7 +125,7 @@ jobs:
           cache: 'maven'
       - name: Install
         run: >-
-          ./mvnw -B -q install -DskipTests
+          ./mvnw -B -q install -DskipTests -P release
           -D"maven.test.skip"=true
           -D"maven.javadoc.skip"=true
           -D"checkstyle.skip"=true
diff --git a/pom.xml b/pom.xml
index e5195ce0..070562e0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -100,7 +100,6 @@
         <skipUT>false</skipUT>
 
         <!-- dependency -->
-        <seatunnel-common.version>2.1.3</seatunnel-common.version>
         <commons.logging.version>1.2</commons.logging.version>
         <slf4j.version>1.7.25</slf4j.version>
         <jackson.version>2.12.7.1</jackson.version>
@@ -113,14 +112,157 @@
         <checker.qual.version>3.10.0</checker.qual.version>
         <log4j-core.version>2.17.1</log4j-core.version>
         <awaitility.version>4.2.0</awaitility.version>
+        <seatunnel-framework.version>2.3.1</seatunnel-framework.version>
     </properties>
 
     <dependencyManagement>
         <dependencies>
+            <!-- seatunnel main repository dependency -->
             <dependency>
                 <groupId>org.apache.seatunnel</groupId>
                 <artifactId>seatunnel-common</artifactId>
-                <version>${seatunnel-common.version}</version>
+                <version>${seatunnel-framework.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>jcl-over-slf4j</artifactId>
+                        <groupId>org.slf4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-1.2-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-core</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-slf4j-impl</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-jackson</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <classifier>optional</classifier>
+                <exclusions>
+                    <exclusion>
+                        <groupId>com.fasterxml.jackson.dataformat</groupId>
+                        <artifactId>jackson-dataformat-properties</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>slf4j-log4j12</artifactId>
+                        <groupId>org.slf4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-1.2-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>jcl-over-slf4j</artifactId>
+                        <groupId>org.slf4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-core</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-slf4j-impl</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-api</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <artifactId>jcl-over-slf4j</artifactId>
+                        <groupId>org.slf4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-1.2-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-api</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-core</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                    <exclusion>
+                        <artifactId>log4j-slf4j-impl</artifactId>
+                        <groupId>org.apache.logging.log4j</groupId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-engine-client</artifactId>
+                <version>${seatunnel-framework.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.google.auto.service</groupId>
+                <artifactId>auto-service</artifactId>
+                <version>${auto-service.version}</version>
+                <scope>provided</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-plugin-discovery</artifactId>
+                <version>${seatunnel-framework.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-transforms-v2</artifactId>
+                <version>${seatunnel-framework.version}</version>
+            </dependency>
+
+            <!-- seatunnel connector test -->
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>connector-common</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <scope>test</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>connector-console</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <scope>test</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>connector-fake</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <scope>test</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>connector-jdbc</artifactId>
+                <version>${seatunnel-framework.version}</version>
+                <scope>test</scope>
             </dependency>
 
             <dependency>
@@ -153,7 +295,7 @@
                 <artifactId>jackson-databind</artifactId>
                 <version>${jackson.version}</version>
             </dependency>
-            
+
             <dependency>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
diff --git a/seatunnel-server/pom.xml b/seatunnel-server/pom.xml
index 79ef3a31..d6bd26df 100644
--- a/seatunnel-server/pom.xml
+++ b/seatunnel-server/pom.xml
@@ -27,6 +27,7 @@
     <packaging>pom</packaging>
     <modules>
         <module>seatunnel-app</module>
+        <module>seatunnel-dynamicform</module>
         <module>seatunnel-spi</module>
         <module>seatunnel-scheduler</module>
         <module>seatunnel-server-common</module>
diff --git a/seatunnel-server/seatunnel-app/pom.xml b/seatunnel-server/seatunnel-app/pom.xml
index aa41a158..0e94b654 100644
--- a/seatunnel-server/seatunnel-app/pom.xml
+++ b/seatunnel-server/seatunnel-app/pom.xml
@@ -93,6 +93,10 @@
                     <artifactId>spring-context</artifactId>
                     <groupId>org.springframework</groupId>
                 </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-annotations</artifactId>
+                </exclusion>
             </exclusions>
         </dependency>
 
diff --git a/seatunnel-server/seatunnel-app/src/main/resources/application.yml b/seatunnel-server/seatunnel-app/src/main/resources/application.yml
index 7d9f51af..a873c088 100644
--- a/seatunnel-server/seatunnel-app/src/main/resources/application.yml
+++ b/seatunnel-server/seatunnel-app/src/main/resources/application.yml
@@ -40,7 +40,7 @@ ds:
     default: gaojun
   api:
     token: 296fecc6e5f78e6a5aa39106707476fe
-    prefix: http://127.0.0.1:12345/dolphinscheduler
+    prefix: http://localhost:12345/dolphinscheduler
 
 jwt:
   expireTime: 86400
diff --git a/seatunnel-server/seatunnel-app/src/test/java/org/apache/seatunnel/app/controller/UserControllerTest.java b/seatunnel-server/seatunnel-app/src/test/java/org/apache/seatunnel/app/controller/UserControllerTest.java
index 4b1b9116..20b706c9 100644
--- a/seatunnel-server/seatunnel-app/src/test/java/org/apache/seatunnel/app/controller/UserControllerTest.java
+++ b/seatunnel-server/seatunnel-app/src/test/java/org/apache/seatunnel/app/controller/UserControllerTest.java
@@ -28,7 +28,8 @@ import org.apache.seatunnel.app.domain.request.user.AddUserReq;
 import org.apache.seatunnel.app.domain.response.user.AddUserRes;
 import org.apache.seatunnel.common.utils.JsonUtils;
 
-import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.core.type.TypeReference;
+
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -38,6 +39,8 @@ import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MvcResult;
 import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
 
+import java.lang.reflect.Type;
+
 @Disabled("todo:this test is not working, waiting fix")
 public class UserControllerTest extends WebMvcApplicationTest {
 
@@ -59,6 +62,10 @@ public class UserControllerTest extends WebMvcApplicationTest {
                 .andReturn();
         String result = mvcResult.getResponse().getContentAsString();
         Result<AddUserRes> data = JsonUtils.parseObject(result, new TypeReference<Result<AddUserRes>>() {
+            @Override
+            public Type getType() {
+                return super.getType();
+            }
         });
         Assertions.assertTrue(data.isSuccess());
         Assertions.assertNotNull(data.getData());
diff --git a/seatunnel-server/seatunnel-server-common/pom.xml b/seatunnel-server/seatunnel-dynamicform/pom.xml
similarity index 52%
copy from seatunnel-server/seatunnel-server-common/pom.xml
copy to seatunnel-server/seatunnel-dynamicform/pom.xml
index 6eb22aac..36376f75 100644
--- a/seatunnel-server/seatunnel-server-common/pom.xml
+++ b/seatunnel-server/seatunnel-dynamicform/pom.xml
@@ -13,27 +13,45 @@
     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"
+<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>seatunnel-server</artifactId>
         <groupId>org.apache.seatunnel</groupId>
         <version>1.0.0-SNAPSHOT</version>
     </parent>
-    <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>seatunnel-server-common</artifactId>
-
-    <properties>
-        <java.version>1.8</java.version>
-    </properties>
+    <artifactId>seatunnel-dynamicform</artifactId>
 
     <dependencies>
         <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
-            <scope>provided</scope>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-common</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-jackson</artifactId>
+            <classifier>optional</classifier>
         </dependency>
     </dependencies>
-</project>
\ No newline at end of file
+</project>
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormOption.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormOption.java
new file mode 100644
index 00000000..4630bbb3
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormOption.java
@@ -0,0 +1,118 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.app.dynamicforms.validate.AbstractValidate;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public abstract class AbstractFormOption<T extends AbstractFormOption, V extends AbstractValidate> {
+
+    // support i18n
+    private final String label;
+    private final String field;
+    private Object defaultValue;
+
+    // support i18n
+    private String description = "";
+    private boolean clearable;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private Map<String, Object> show;
+
+    // support i18n
+    private String placeholder = "";
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private V validate;
+
+    public AbstractFormOption(@NonNull String label, @NonNull String field) {
+        this.label = label;
+        this.field = field;
+    }
+
+    public enum FormType {
+        @JsonProperty("input")
+        INPUT("input"),
+
+        @JsonProperty("select")
+        SELECT("select");
+
+        @Getter
+        private String formType;
+
+        FormType(String formType) {
+            this.formType = formType;
+        }
+    }
+
+    public T withShow(@NonNull String field, @NonNull List<Object> values) {
+        if (this.show == null) {
+            this.show = new HashMap<>();
+        }
+
+        this.show.put(Constants.SHOW_FIELD, field);
+        this.show.put(Constants.SHOW_VALUE, values);
+        return (T) this;
+    }
+
+    public T withValidate(@NonNull V validate) {
+        this.validate = validate;
+        return (T) this;
+    }
+
+    public T withDefaultValue(Object defaultValue) {
+        this.defaultValue = defaultValue;
+        return (T) this;
+    }
+
+    public T withDescription(@NonNull String description) {
+        this.description = description;
+        return (T) this;
+    }
+
+    public T withI18nDescription(@NonNull String description) {
+        this.description = FormLocale.I18N_PREFIX + description;
+        return (T) this;
+    }
+
+    public T withClearable() {
+        this.clearable = true;
+        return (T) this;
+    }
+
+    public T withPlaceholder(@NonNull String placeholder) {
+        this.placeholder = placeholder;
+        return (T) this;
+    }
+
+    public T withI18nPlaceholder(@NonNull String placeholder) {
+        this.placeholder = FormLocale.I18N_PREFIX + placeholder;
+        return (T) this;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormSelectOption.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormSelectOption.java
new file mode 100644
index 00000000..52b0c534
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormSelectOption.java
@@ -0,0 +1,49 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Getter;
+import lombok.NonNull;
+
+public abstract class AbstractFormSelectOption extends AbstractFormOption {
+
+    @JsonProperty("type")
+    @Getter
+    private final FormType formType = FormType.SELECT;
+
+    public AbstractFormSelectOption(@NonNull String label, @NonNull String field) {
+        super(label, field);
+    }
+
+    public static class SelectOption {
+        @JsonProperty
+        @Getter
+        private String label;
+
+        @JsonProperty
+        @Getter
+        private Object value;
+
+        public SelectOption(@NonNull String label, @NonNull Object value) {
+            this.label = label;
+            this.value = value;
+        }
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/Constants.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/Constants.java
new file mode 100644
index 00000000..df4ac2dc
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/Constants.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 org.apache.seatunnel.app.dynamicforms;
+
+public final class Constants {
+
+    public static final String SHOW_FIELD = "field";
+
+    public static final String SHOW_VALUE = "value";
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/DynamicSelectOption.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/DynamicSelectOption.java
new file mode 100644
index 00000000..d1365cd6
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/DynamicSelectOption.java
@@ -0,0 +1,33 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+
+public class DynamicSelectOption extends AbstractFormSelectOption {
+    @Getter
+    @Setter
+    private String api;
+
+    public DynamicSelectOption(@NonNull String api, @NonNull String label, @NonNull String field) {
+        super(label, field);
+        this.api = api;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormInputOption.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormInputOption.java
new file mode 100644
index 00000000..f71d64ac
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormInputOption.java
@@ -0,0 +1,56 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Getter;
+import lombok.NonNull;
+
+public class FormInputOption extends AbstractFormOption {
+    @JsonProperty("type")
+    @Getter
+    private final FormType formType = FormType.INPUT;
+
+    @Getter
+    private final InputType inputType;
+
+    public FormInputOption(
+        @NonNull InputType inputType, @NonNull String label, @NonNull String field) {
+        super(label, field);
+        this.inputType = inputType;
+    }
+
+    public enum InputType {
+        @JsonProperty("text")
+        TEXT("text"),
+
+        @JsonProperty("password")
+        PASSWORD("password"),
+
+        @JsonProperty("textarea")
+        TEXTAREA("textarea");
+
+        @Getter
+        private String inputType;
+
+        InputType(String inputType) {
+            this.inputType = inputType;
+        }
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormLocale.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormLocale.java
new file mode 100644
index 00000000..466b03c6
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormLocale.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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonIgnore;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.LinkedHashMap;
+
+/**
+ * Multi language support
+ */
+@Data
+public class FormLocale {
+    @JsonIgnore
+    public static final String I18N_PREFIX = "i18n.";
+
+    @JsonProperty("zh_CN")
+    private LinkedHashMap<String, String> zhCN = new LinkedHashMap<>();
+
+    @JsonProperty("en_US")
+    private LinkedHashMap<String, String> enUS = new LinkedHashMap<>();
+
+    public FormLocale addZhCN(@NonNull String key, @NonNull String value) {
+        zhCN.put(key, value);
+        return this;
+    }
+
+    public FormLocale addEnUS(@NonNull String key, @NonNull String value) {
+        enUS.put(key, value);
+        return this;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormOptionBuilder.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormOptionBuilder.java
new file mode 100644
index 00000000..8c645abf
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormOptionBuilder.java
@@ -0,0 +1,165 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import lombok.NonNull;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FormOptionBuilder {
+
+    private String label;
+
+    private String field;
+
+    public static FormOptionBuilder builder() {
+        return new FormOptionBuilder();
+    }
+
+    public FormOptionBuilder withLabel(@NonNull String label) {
+        this.label = label;
+        return this;
+    }
+
+    public FormOptionBuilder withI18nLabel(@NonNull String label) {
+        this.label = FormLocale.I18N_PREFIX + label;
+        return this;
+    }
+
+    public FormOptionBuilder withField(@NonNull String field) {
+        this.field = field;
+        return this;
+    }
+
+    public InputOptionBuilder inputOptionBuilder() {
+        return new InputOptionBuilder(label, field);
+    }
+
+    public DynamicSelectOptionBuilder dynamicSelectOptionBuilder() {
+        return new DynamicSelectOptionBuilder(label, field);
+    }
+
+    public StaticSelectOptionBuilder staticSelectOptionBuilder() {
+        return new StaticSelectOptionBuilder(label, field);
+    }
+
+    public static class InputOptionBuilder {
+        private String label;
+
+        private String field;
+
+        public InputOptionBuilder(@NonNull String label, @NonNull String field) {
+            this.label = label;
+            this.field = field;
+        }
+
+        public FormInputOption formTextInputOption() {
+            return new FormInputOption(FormInputOption.InputType.TEXT, label, field);
+        }
+
+        public FormInputOption formPasswordInputOption() {
+            return new FormInputOption(FormInputOption.InputType.PASSWORD, label, field);
+        }
+
+        public FormInputOption formTextareaInputOption() {
+            return new FormInputOption(FormInputOption.InputType.TEXTAREA, label, field);
+        }
+    }
+
+    public static class DynamicSelectOptionBuilder {
+        private String label;
+
+        private String field;
+
+        private String selectApi;
+
+        public DynamicSelectOptionBuilder(@NonNull String label, @NonNull String field) {
+            this.label = label;
+            this.field = field;
+        }
+
+        public DynamicSelectOptionBuilder withSelectApi(@NonNull String selectApi) {
+            this.selectApi = selectApi;
+            return this;
+        }
+
+        public DynamicSelectOption formDynamicSelectOption() {
+            return new DynamicSelectOption(selectApi, label, field);
+        }
+    }
+
+    public static class StaticSelectOptionBuilder {
+        private String label;
+
+        private String field;
+
+        private List<AbstractFormSelectOption.SelectOption> options = new ArrayList<>();
+
+        public StaticSelectOptionBuilder(@NonNull String label, @NonNull String field) {
+            this.label = label;
+            this.field = field;
+        }
+
+        public StaticSelectOptionBuilder addSelectOptions(
+            @NonNull List<ImmutablePair> selectOptions) {
+            for (ImmutablePair option : selectOptions) {
+                options.add(
+                    new AbstractFormSelectOption.SelectOption(
+                        option.left.toString(), option.right.toString()));
+            }
+            return this;
+        }
+
+        public StaticSelectOptionBuilder addI18nSelectOptions(
+            @NonNull List<ImmutablePair> selectOptions) {
+            for (ImmutablePair option : selectOptions) {
+                options.add(
+                    new AbstractFormSelectOption.SelectOption(
+                        FormLocale.I18N_PREFIX + option.left.toString(),
+                        option.right.toString()));
+            }
+            return this;
+        }
+
+        public StaticSelectOptionBuilder addSelectOptions(@NonNull ImmutablePair... selectOptions) {
+            for (ImmutablePair option : selectOptions) {
+                options.add(
+                    new AbstractFormSelectOption.SelectOption(
+                        option.left.toString(), option.right.toString()));
+            }
+            return this;
+        }
+
+        public StaticSelectOptionBuilder addI18nSelectOptions(
+            @NonNull ImmutablePair... selectOptions) {
+            for (ImmutablePair option : selectOptions) {
+                options.add(
+                    new AbstractFormSelectOption.SelectOption(
+                        FormLocale.I18N_PREFIX + option.left.toString(),
+                        option.right.toString()));
+            }
+            return this;
+        }
+
+        public StaticSelectOption formStaticSelectOption() {
+            return new StaticSelectOption(options, label, field);
+        }
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructure.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructure.java
new file mode 100644
index 00000000..6096b596
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructure.java
@@ -0,0 +1,65 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonIgnoreType;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonInclude;
+
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.List;
+import java.util.Map;
+
+/** SeaTunnel Web UI will use this json data to automatically create page form elements */
+@Data
+public class FormStructure {
+    private String name;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private FormLocale locales;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private Map<String, Map<String, String>> apis;
+
+    private List<AbstractFormOption> forms;
+
+    public FormStructure() {}
+
+    public FormStructure(
+            @NonNull String name,
+            @NonNull List<AbstractFormOption> formOptionList,
+            FormLocale locale,
+            Map<String, Map<String, String>> apis) {
+        this.name = name;
+        this.forms = formOptionList;
+        this.locales = locale;
+        this.apis = apis;
+    }
+
+    @JsonIgnoreType
+    public enum HttpMethod {
+        GET,
+
+        POST
+    }
+
+    public static FormStructureBuilder builder() {
+        return new FormStructureBuilder();
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilder.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilder.java
new file mode 100644
index 00000000..ba863680
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilder.java
@@ -0,0 +1,74 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.app.dynamicforms.exception.FormStructureValidateException;
+
+import lombok.NonNull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class FormStructureBuilder {
+    private String name;
+
+    private List<AbstractFormOption> forms = new ArrayList<>();
+
+    private FormLocale locales;
+
+    private Map<String, Map<String, String>> apis;
+
+    public FormStructureBuilder name(@NonNull String name) {
+        this.name = name;
+        return this;
+    }
+
+    public FormStructureBuilder addFormOption(@NonNull AbstractFormOption... formOptions) {
+        for (AbstractFormOption formOption : formOptions) {
+            forms.add(formOption);
+        }
+        return this;
+    }
+
+    public FormStructureBuilder withLocale(FormLocale locale) {
+        this.locales = locale;
+        return this;
+    }
+
+    public FormStructureBuilder addApi(
+            @NonNull String apiName,
+            @NonNull String url,
+            @NonNull FormStructure.HttpMethod method) {
+        if (apis == null) {
+            apis = new HashMap<>();
+        }
+        apis.putIfAbsent(apiName, new HashMap<>());
+        apis.get(apiName).put("url", url);
+        apis.get(apiName).put("method", method.name().toLowerCase(java.util.Locale.ROOT));
+
+        return this;
+    }
+
+    public FormStructure build() throws FormStructureValidateException {
+        FormStructure formStructure = new FormStructure(name, forms, locales, apis);
+        FormStructureValidate.validateFormStructure(formStructure);
+        return formStructure;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureValidate.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureValidate.java
new file mode 100644
index 00000000..025fe313
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureValidate.java
@@ -0,0 +1,273 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.app.dynamicforms.exception.FormStructureValidateException;
+import org.apache.seatunnel.app.dynamicforms.validate.AbstractValidate;
+import org.apache.seatunnel.app.dynamicforms.validate.MutuallyExclusiveValidate;
+import org.apache.seatunnel.app.dynamicforms.validate.UnionNonEmptyValidate;
+
+import lombok.NonNull;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Check whether the form structure is correct
+ */
+public class FormStructureValidate {
+
+    /**
+     * validate rules
+     */
+    public static void validateFormStructure(@NonNull FormStructure formStructure)
+        throws FormStructureValidateException {
+
+        List<String> apiErrorList = validateApiOption(formStructure);
+        List<String> localeErrorList = validateLocaleOption(formStructure);
+        List<String> showErrorList = validateShow(formStructure);
+        List<String> unionNonErrorList = validateUnionNonEmpty(formStructure);
+        List<String> exclusiveErrorList = validateMutuallyExclusive(formStructure);
+
+        apiErrorList.addAll(localeErrorList);
+        apiErrorList.addAll(showErrorList);
+        apiErrorList.addAll(unionNonErrorList);
+        apiErrorList.addAll(exclusiveErrorList);
+
+        if (apiErrorList.size() > 0) {
+            throw new FormStructureValidateException(formStructure.getName(), apiErrorList);
+        }
+    }
+
+    private static List<String> validateApiOption(@NonNull FormStructure formStructure) {
+        List<String> errorMessageList = new ArrayList();
+        Map<String, Map<String, String>> apis = formStructure.getApis();
+        formStructure
+            .getForms()
+            .forEach(
+                formOption -> {
+                    if (formOption instanceof DynamicSelectOption) {
+                        String api = ((DynamicSelectOption) formOption).getApi();
+                        if (apis == null || !apis.keySet().contains(api)) {
+                            errorMessageList.add(
+                                String.format(
+                                    "DynamicSelectOption[%s] used api[%s] can not found in FormStructure.apis",
+                                    ((DynamicSelectOption) formOption).getLabel(),
+                                    api));
+                        }
+                    }
+                });
+        return errorMessageList;
+    }
+
+    private static List<String> validateLocaleOption(@NonNull FormStructure formStructure) {
+        List<String> errorMessageList = new ArrayList();
+        FormLocale locales = formStructure.getLocales();
+        formStructure
+            .getForms()
+            .forEach(
+                formOption -> {
+                    if (formOption.getLabel().startsWith(FormLocale.I18N_PREFIX)) {
+                        String labelName =
+                            formOption.getLabel().replace(FormLocale.I18N_PREFIX, "");
+                        validateOneI18nOption(
+                            locales,
+                            formOption.getLabel(),
+                            "label",
+                            labelName,
+                            errorMessageList);
+                    }
+
+                    if (formOption.getDescription().startsWith(FormLocale.I18N_PREFIX)) {
+                        String description =
+                            formOption
+                                .getDescription()
+                                .replace(FormLocale.I18N_PREFIX, "");
+                        validateOneI18nOption(
+                            locales,
+                            formOption.getLabel(),
+                            "description",
+                            description,
+                            errorMessageList);
+                    }
+
+                    if (formOption.getPlaceholder().startsWith(FormLocale.I18N_PREFIX)) {
+                        String placeholder =
+                            formOption
+                                .getPlaceholder()
+                                .replace(FormLocale.I18N_PREFIX, "");
+                        validateOneI18nOption(
+                            locales,
+                            formOption.getLabel(),
+                            "placeholder",
+                            placeholder,
+                            errorMessageList);
+                    }
+
+                    AbstractValidate validate = formOption.getValidate();
+                    if (validate != null
+                        && validate.getMessage().startsWith(FormLocale.I18N_PREFIX)) {
+                        String message =
+                            validate.getMessage().replace(FormLocale.I18N_PREFIX, "");
+                        validateOneI18nOption(
+                            locales,
+                            formOption.getLabel(),
+                            "validateMessage",
+                            message,
+                            errorMessageList);
+                    }
+                });
+        return errorMessageList;
+    }
+
+    private static void validateOneI18nOption(
+        FormLocale locale,
+        @NonNull String formOptionLabel,
+        @NonNull String formOptionName,
+        @NonNull String key,
+        @NonNull List<String> errorMessageList) {
+        if (locale == null || !locale.getEnUS().containsKey(key)) {
+            errorMessageList.add(
+                String.format(
+                    "FormOption[%s] used i18n %s[%s] can not found in FormStructure.locales en_US",
+                    formOptionLabel, formOptionName, key));
+        }
+
+        if (locale == null || !locale.getZhCN().containsKey(key)) {
+            errorMessageList.add(
+                String.format(
+                    "FormOption[%s] used i18n %s[%s] can not found in FormStructure.locales zh_CN",
+                    formOptionLabel, formOptionName, key));
+        }
+    }
+
+    private static List<String> validateShow(@NonNull FormStructure formStructure) {
+        List<String> errorMessageList = new ArrayList();
+        // Find all select options
+        List<String> allFields =
+            formStructure.getForms().stream()
+                .map(formOption -> formOption.getField())
+                .collect(Collectors.toList());
+        formStructure
+            .getForms()
+            .forEach(
+                formOption -> {
+                    Map show = formOption.getShow();
+                    if (show == null) {
+                        return;
+                    }
+
+                    String field = show.get("field").toString();
+                    if (allFields == null || !allFields.contains(field)) {
+                        errorMessageList.add(
+                            String.format(
+                                "FormOption[%s] used show field[%s] can not found in form options",
+                                formOption.getLabel(), field));
+                    }
+                });
+
+        return errorMessageList;
+    }
+
+    private static List<String> validateUnionNonEmpty(@NonNull FormStructure formStructure) {
+        List<String> errorMessageList = new ArrayList();
+        Map<String, List<String>> unionMap = new HashMap<>();
+        // find all union-non-empty options
+        formStructure
+            .getForms()
+            .forEach(
+                formOption -> {
+                    if (formOption.getValidate() != null
+                        && formOption.getValidate() instanceof UnionNonEmptyValidate) {
+                        unionMap.put(
+                            formOption.getField(),
+                            ((UnionNonEmptyValidate) formOption.getValidate())
+                                .getFields());
+                    }
+                });
+
+        unionMap.forEach(
+            (k, v) -> {
+                if (v == null || !v.contains(k)) {
+                    errorMessageList.add(
+                        String.format(
+                            "UnionNonEmptyValidate Option field[%s] must in validate union field list",
+                            k));
+                }
+
+                if (v != null) {
+                    v.forEach(
+                        field -> {
+                            if (!unionMap.keySet().contains(field)) {
+                                errorMessageList.add(
+                                    String.format(
+                                        "UnionNonEmptyValidate Option field[%s] , validate union field[%s] can not found in form options",
+                                        k, field));
+                            }
+                        });
+                }
+            });
+
+        return errorMessageList;
+    }
+
+    private static List<String> validateMutuallyExclusive(@NonNull FormStructure formStructure) {
+        List<String> errorMessageList = new ArrayList();
+        Map<String, List<String>> exclusiveMap = new HashMap<>();
+        // find all mutually-exclusive options
+        formStructure
+            .getForms()
+            .forEach(
+                formOption -> {
+                    if (formOption.getValidate() != null
+                        && formOption.getValidate() instanceof MutuallyExclusiveValidate) {
+                        exclusiveMap.put(
+                            formOption.getField(),
+                            ((MutuallyExclusiveValidate) formOption.getValidate())
+                                .getFields());
+                    }
+                });
+
+        exclusiveMap.forEach(
+            (k, v) -> {
+                if (v == null || !v.contains(k)) {
+                    errorMessageList.add(
+                        String.format(
+                            "MutuallyExclusiveValidate Option field[%s] must in validate field list",
+                            k));
+                }
+
+                if (v != null) {
+                    v.forEach(
+                        field -> {
+                            if (!exclusiveMap.keySet().contains(field)) {
+                                errorMessageList.add(
+                                    String.format(
+                                        "MutuallyExclusiveValidate Option field[%s] , validate field[%s] can not found in form options",
+                                        k, field));
+                            }
+                        });
+                }
+            });
+
+        return errorMessageList;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/StaticSelectOption.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/StaticSelectOption.java
new file mode 100644
index 00000000..e70f4a24
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/StaticSelectOption.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 org.apache.seatunnel.app.dynamicforms;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class StaticSelectOption extends AbstractFormSelectOption {
+
+    @Getter
+    @Setter
+    private List<SelectOption> options = new ArrayList<>();
+
+    public StaticSelectOption(
+        @NonNull List<SelectOption> options, @NonNull String label, @NonNull String field) {
+        super(label, field);
+        this.options = options;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/exception/FormStructureValidateException.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/exception/FormStructureValidateException.java
new file mode 100644
index 00000000..02804e32
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/exception/FormStructureValidateException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.seatunnel.app.dynamicforms.exception;
+
+import lombok.NonNull;
+
+import java.util.List;
+
+public class FormStructureValidateException extends RuntimeException {
+
+    public FormStructureValidateException(
+            @NonNull String formName, @NonNull List<String> errorList, @NonNull Throwable e) {
+        super(String.format("Form: %s, validate error - %s", formName, errorList), e);
+    }
+
+    public FormStructureValidateException(
+            @NonNull String formName, @NonNull List<String> errorList) {
+        super(String.format("Form: %s, validate error - %s", formName, errorList));
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/AbstractValidate.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/AbstractValidate.java
new file mode 100644
index 00000000..f5576611
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/AbstractValidate.java
@@ -0,0 +1,65 @@
+/*
+ * 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.seatunnel.app.dynamicforms.validate;
+
+import org.apache.seatunnel.app.dynamicforms.FormLocale;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Data
+public class AbstractValidate<T extends AbstractValidate> {
+    private final List<String> trigger = Arrays.asList("input", "blur");
+
+    // support i18n
+    private String message = "required";
+
+    public enum RequiredType {
+        @JsonProperty("non-empty")
+        NON_EMPTY("non-empty"),
+
+        @JsonProperty("union-non-empty")
+        UNION_NON_EMPTY("union-non-empty"),
+
+        @JsonProperty("mutually-exclusive")
+        MUTUALLY_EXCLUSIVE("mutually-exclusive");
+
+        @Getter
+        private String type;
+
+        RequiredType(String type) {
+            this.type = type;
+        }
+    }
+
+    public T withMessage(@NonNull String message) {
+        this.message = message;
+        return (T) this;
+    }
+
+    public T withI18nMessage(@NonNull String message) {
+        this.message = FormLocale.I18N_PREFIX + message;
+        return (T) this;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/MutuallyExclusiveValidate.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/MutuallyExclusiveValidate.java
new file mode 100644
index 00000000..a223d9df
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/MutuallyExclusiveValidate.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 org.apache.seatunnel.app.dynamicforms.validate;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.List;
+
+@Data
+public class MutuallyExclusiveValidate extends AbstractValidate {
+    private final boolean required = false;
+    private List<String> fields;
+
+    @JsonProperty("type")
+    private final RequiredType requiredType = RequiredType.MUTUALLY_EXCLUSIVE;
+
+    public MutuallyExclusiveValidate(@NonNull List<String> fields) {
+        checkArgument(fields.size() > 0);
+        this.fields = fields;
+        this.withMessage("parameters:" + fields + ", only one can be set");
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/NonEmptyValidate.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/NonEmptyValidate.java
new file mode 100644
index 00000000..0ee27cbb
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/NonEmptyValidate.java
@@ -0,0 +1,30 @@
+/*
+ * 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.seatunnel.app.dynamicforms.validate;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+
+@Data
+public class NonEmptyValidate extends AbstractValidate {
+    private final boolean required = true;
+
+    @JsonProperty("type")
+    private final RequiredType requiredType = RequiredType.NON_EMPTY;
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/UnionNonEmptyValidate.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/UnionNonEmptyValidate.java
new file mode 100644
index 00000000..8328459d
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/UnionNonEmptyValidate.java
@@ -0,0 +1,45 @@
+/*
+ * 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.seatunnel.app.dynamicforms.validate;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.apache.seatunnel.shade.com.fasterxml.jackson.annotation.JsonProperty;
+
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.List;
+
+@Data
+public class UnionNonEmptyValidate extends AbstractValidate {
+    private final boolean required = false;
+    private List<String> fields;
+
+    @JsonProperty("type")
+    private final RequiredType requiredType = RequiredType.UNION_NON_EMPTY;
+
+    public UnionNonEmptyValidate(@NonNull List<String> fields) {
+        checkArgument(fields.size() > 0);
+        this.fields = fields;
+        this.withMessage(
+            "parameters:"
+                + fields
+                + " if any of these parameters is set, other parameters must also be set");
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/ValidateBuilder.java b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/ValidateBuilder.java
new file mode 100644
index 00000000..53a6e94f
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/ValidateBuilder.java
@@ -0,0 +1,78 @@
+/*
+ * 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.seatunnel.app.dynamicforms.validate;
+
+import lombok.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ValidateBuilder {
+
+    public static ValidateBuilder builder() {
+        return new ValidateBuilder();
+    }
+
+    public NonEmptyValidateBuilder nonEmptyValidateBuilder() {
+        return new NonEmptyValidateBuilder();
+    }
+
+    public UnionNonEmptyValidateBuilder unionNonEmptyValidateBuilder() {
+        return new UnionNonEmptyValidateBuilder();
+    }
+
+    public MutuallyExclusiveValidateBuilder mutuallyExclusiveValidateBuilder() {
+        return new MutuallyExclusiveValidateBuilder();
+    }
+
+    public static class NonEmptyValidateBuilder {
+        public NonEmptyValidate nonEmptyValidate() {
+            return new NonEmptyValidate();
+        }
+    }
+
+    public static class UnionNonEmptyValidateBuilder {
+        private List<String> fields = new ArrayList<>();
+
+        public UnionNonEmptyValidateBuilder fields(@NonNull String... fields) {
+            for (String field : fields) {
+                this.fields.add(field);
+            }
+            return this;
+        }
+
+        public UnionNonEmptyValidate unionNonEmptyValidate() {
+            return new UnionNonEmptyValidate(fields);
+        }
+    }
+
+    public static class MutuallyExclusiveValidateBuilder {
+        private List<String> fields = new ArrayList<>();
+
+        public MutuallyExclusiveValidateBuilder fields(@NonNull String... fields) {
+            for (String field : fields) {
+                this.fields.add(field);
+            }
+            return this;
+        }
+
+        public MutuallyExclusiveValidate mutuallyExclusiveValidate() {
+            return new MutuallyExclusiveValidate(fields);
+        }
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilderTest.java b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilderTest.java
new file mode 100644
index 00000000..cbea72a8
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilderTest.java
@@ -0,0 +1,329 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import org.apache.seatunnel.app.dynamicforms.exception.FormStructureValidateException;
+import org.apache.seatunnel.app.dynamicforms.validate.ValidateBuilder;
+import org.apache.seatunnel.common.utils.FileUtils;
+import org.apache.seatunnel.common.utils.JsonUtils;
+
+import org.apache.commons.lang3.tuple.ImmutablePair;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+public class FormStructureBuilderTest {
+
+    @Test
+    public void testFormStructureBuild() {
+        FormLocale locale = new FormLocale();
+        locale.addZhCN("name_password_union_required", "all name and password are required")
+            .addZhCN("username", "username")
+            .addEnUS("name_password_union_required", "all name and password are required")
+            .addEnUS("username", "username");
+
+        FormInputOption nameOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withI18nLabel("username")
+                    .withField("username")
+                    .inputOptionBuilder()
+                    .formTextInputOption()
+                    .withDescription("username")
+                    .withClearable()
+                    .withPlaceholder("username")
+                    .withShow("checkType", Arrays.asList("nameAndPassword"))
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .unionNonEmptyValidateBuilder()
+                            .fields("username", "password")
+                            .unionNonEmptyValidate()
+                            .withI18nMessage("name_password_union_required"));
+
+        FormInputOption passwordOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withLabel("password")
+                    .withField("password")
+                    .inputOptionBuilder()
+                    .formPasswordInputOption()
+                    .withDescription("password")
+                    .withPlaceholder("password")
+                    .withShow("checkType", Arrays.asList("nameAndPassword"))
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .unionNonEmptyValidateBuilder()
+                            .fields("username", "password")
+                            .unionNonEmptyValidate()
+                            .withI18nMessage("name_password_union_required"));
+
+        FormInputOption textAreaOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withLabel("content")
+                    .withField("context")
+                    .inputOptionBuilder()
+                    .formTextareaInputOption()
+                    .withClearable()
+                    .withDescription("content");
+
+        StaticSelectOption checkTypeOption =
+            (StaticSelectOption)
+                FormOptionBuilder.builder()
+                    .withLabel("checkType")
+                    .withField("checkType")
+                    .staticSelectOptionBuilder()
+                    .addSelectOptions(
+                        new ImmutablePair("no", "no"),
+                        new ImmutablePair("nameAndPassword", "nameAndPassword"))
+                    .formStaticSelectOption()
+                    .withClearable()
+                    .withDefaultValue("no")
+                    .withDescription("check type")
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .nonEmptyValidateBuilder()
+                            .nonEmptyValidate());
+
+        DynamicSelectOption cityOption =
+            (DynamicSelectOption)
+                FormOptionBuilder.builder()
+                    .withField("city")
+                    .withLabel("city")
+                    .dynamicSelectOptionBuilder()
+                    .withSelectApi("getCity")
+                    .formDynamicSelectOption()
+                    .withDescription("city")
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .nonEmptyValidateBuilder()
+                            .nonEmptyValidate());
+
+        AbstractFormOption exclusive1 =
+            FormOptionBuilder.builder()
+                .withField("exclusive1")
+                .withLabel("exclusive1")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive2")
+                        .mutuallyExclusiveValidate());
+
+        AbstractFormOption exclusive2 =
+            FormOptionBuilder.builder()
+                .withField("exclusive2")
+                .withLabel("exclusive2")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive2")
+                        .mutuallyExclusiveValidate());
+
+        FormStructure testForm =
+            FormStructure.builder()
+                .name("testForm")
+                .addFormOption(
+                    nameOption,
+                    passwordOption,
+                    textAreaOption,
+                    checkTypeOption,
+                    cityOption,
+                    exclusive1,
+                    exclusive2)
+                .withLocale(locale)
+                .addApi("getCity", "/api/get_city", FormStructure.HttpMethod.GET)
+                .build();
+
+        String s = JsonUtils.toJsonString(testForm);
+        String templateFilePath = TestUtils.getResource("test_form.json");
+        String result = FileUtils.readFileToStr(Paths.get(templateFilePath));
+        Assertions.assertEquals(result, s);
+    }
+
+    @Test
+    public void testFormStructureValidate() {
+        FormLocale locale = new FormLocale();
+        locale.addZhCN("name_password_union_required", "all name and password are required")
+            .addEnUS("name_password_union_required", "all name and password are required")
+            .addEnUS("username", "username");
+
+        FormInputOption nameOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withI18nLabel("username")
+                    .withField("username")
+                    .inputOptionBuilder()
+                    .formTextInputOption()
+                    .withDescription("username")
+                    .withClearable()
+                    .withPlaceholder("username")
+                    .withShow("checkType1", Arrays.asList("nameAndPassword"))
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .unionNonEmptyValidateBuilder()
+                            .fields("user", "password")
+                            .unionNonEmptyValidate()
+                            .withI18nMessage("name_password_union_required"));
+
+        FormInputOption passwordOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withLabel("password")
+                    .withField("password")
+                    .inputOptionBuilder()
+                    .formPasswordInputOption()
+                    .withDescription("password")
+                    .withPlaceholder("password")
+                    .withShow("checkType", Arrays.asList("nameAndPassword"))
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .unionNonEmptyValidateBuilder()
+                            .fields("username", "password")
+                            .unionNonEmptyValidate()
+                            .withI18nMessage("name_password_union_required"));
+
+        FormInputOption textAreaOption =
+            (FormInputOption)
+                FormOptionBuilder.builder()
+                    .withLabel("content")
+                    .withField("context")
+                    .inputOptionBuilder()
+                    .formTextareaInputOption()
+                    .withClearable()
+                    .withDescription("content");
+
+        StaticSelectOption checkTypeOption =
+            (StaticSelectOption)
+                FormOptionBuilder.builder()
+                    .withLabel("checkType")
+                    .withField("checkType")
+                    .staticSelectOptionBuilder()
+                    .addSelectOptions(
+                        new ImmutablePair("no", "no"),
+                        new ImmutablePair("nameAndPassword", "nameAndPassword"))
+                    .formStaticSelectOption()
+                    .withClearable()
+                    .withDefaultValue("no")
+                    .withDescription("check type")
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .nonEmptyValidateBuilder()
+                            .nonEmptyValidate());
+
+        DynamicSelectOption cityOption =
+            (DynamicSelectOption)
+                FormOptionBuilder.builder()
+                    .withField("city")
+                    .withLabel("city")
+                    .dynamicSelectOptionBuilder()
+                    .withSelectApi("getCity")
+                    .formDynamicSelectOption()
+                    .withDescription("city")
+                    .withValidate(
+                        ValidateBuilder.builder()
+                            .nonEmptyValidateBuilder()
+                            .nonEmptyValidate());
+
+        AbstractFormOption exclusive1 =
+            FormOptionBuilder.builder()
+                .withField("exclusive1")
+                .withLabel("exclusive1")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive2")
+                        .mutuallyExclusiveValidate());
+
+        AbstractFormOption exclusive2 =
+            FormOptionBuilder.builder()
+                .withField("exclusive2")
+                .withLabel("exclusive2")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive2")
+                        .mutuallyExclusiveValidate());
+
+        AbstractFormOption exclusive3 =
+            FormOptionBuilder.builder()
+                .withField("exclusive3")
+                .withLabel("exclusive3")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive2")
+                        .mutuallyExclusiveValidate());
+
+        AbstractFormOption exclusive4 =
+            FormOptionBuilder.builder()
+                .withField("exclusive4")
+                .withLabel("exclusive4")
+                .inputOptionBuilder()
+                .formTextInputOption()
+                .withValidate(
+                    ValidateBuilder.builder()
+                        .mutuallyExclusiveValidateBuilder()
+                        .fields("exclusive1", "exclusive4", "exclusive5")
+                        .mutuallyExclusiveValidate());
+
+        String error = "";
+        try {
+            FormStructure testForm =
+                FormStructure.builder()
+                    .name("testForm")
+                    .addFormOption(
+                        nameOption,
+                        passwordOption,
+                        textAreaOption,
+                        checkTypeOption,
+                        cityOption,
+                        exclusive1,
+                        exclusive3,
+                        exclusive2,
+                        exclusive4)
+                    .withLocale(locale)
+                    .addApi("getCity1", "/api/get_city", FormStructure.HttpMethod.GET)
+                    .build();
+        } catch (FormStructureValidateException e) {
+            error = e.getMessage();
+        }
+
+        String result =
+            "Form: testForm, validate error - "
+                + "[DynamicSelectOption[city] used api[getCity] can not found in FormStructure.apis, "
+                + "FormOption[i18n.username] used i18n label[username] can not found in FormStructure.locales zh_CN, "
+                + "FormOption[i18n.username] used show field[checkType1] can not found in form options, "
+                + "UnionNonEmptyValidate Option field[username] must in validate union field list, "
+                + "UnionNonEmptyValidate Option field[username] , validate union field[user] can not found in form options, "
+                + "MutuallyExclusiveValidate Option field[exclusive3] must in validate field list, "
+                + "MutuallyExclusiveValidate Option field[exclusive4] , validate field[exclusive5] can not found in form options]";
+        Assertions.assertEquals(result, error);
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/TestUtils.java b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/TestUtils.java
new file mode 100644
index 00000000..e0f94182
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/TestUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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.seatunnel.app.dynamicforms;
+
+import java.io.File;
+
+public class TestUtils {
+    public static String getResource(String confFile) {
+        return System.getProperty("user.dir")
+                + File.separator
+                + "src"
+                + File.separator
+                + "test"
+                + File.separator
+                + "resources"
+                + File.separator
+                + confFile;
+    }
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/src/test/resources/test_form.json b/seatunnel-server/seatunnel-dynamicform/src/test/resources/test_form.json
new file mode 100644
index 00000000..8044981b
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/src/test/resources/test_form.json
@@ -0,0 +1 @@
+{"name":"testForm","locales":{"zh_CN":{"name_password_union_required":"all name and password are required","username":"username"},"en_US":{"name_password_union_required":"all name and password are required","username":"username"}},"apis":{"getCity":{"method":"get","url":"/api/get_city"}},"forms":[{"label":"i18n.username","field":"username","defaultValue":null,"description":"username","clearable":true,"show":{"field":"checkType","value":["nameAndPassword"]},"placeholder":"username","valid [...]
\ No newline at end of file
diff --git a/seatunnel-server/seatunnel-server-common/pom.xml b/seatunnel-server/seatunnel-server-common/pom.xml
index 6eb22aac..35bf91c8 100644
--- a/seatunnel-server/seatunnel-server-common/pom.xml
+++ b/seatunnel-server/seatunnel-server-common/pom.xml
@@ -31,9 +31,10 @@
 
     <dependencies>
         <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
-            <scope>provided</scope>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-jackson</artifactId>
+            <version>${seatunnel-framework.version}</version>
+            <classifier>optional</classifier>
         </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/seatunnel-ui/src/router/routes.ts b/seatunnel-ui/src/router/routes.ts
index 64eac468..19fdd67e 100644
--- a/seatunnel-ui/src/router/routes.ts
+++ b/seatunnel-ui/src/router/routes.ts
@@ -28,7 +28,7 @@ const components: { [key: string]: Component } = utils.mapping(modules)
 
 const basePage: RouteRecordRaw[] = [{
     path: '/',
-    redirect: { name: 'data-pipes' }
+    redirect: { name: 'login' }
   },
   dataPipes, jobs, tasks, userManage]
 
diff --git a/tools/dependencies/known-dependencies.txt b/tools/dependencies/known-dependencies.txt
index 055e9389..5f7dfbe3 100644
--- a/tools/dependencies/known-dependencies.txt
+++ b/tools/dependencies/known-dependencies.txt
@@ -50,9 +50,10 @@ mybatis-plus-boot-starter-3.5.3.1.jar
 mybatis-plus-core-3.5.3.1.jar
 mybatis-plus-extension-3.5.3.1.jar
 scala-library-2.11.12.jar
-seatunnel-common-2.1.3.jar
-seatunnel-config-base-2.1.1.jar
-seatunnel-config-shade-2.1.1.jar
+seatunnel-common-2.3.1.jar
+seatunnel-config-base-2.3.1.jar
+seatunnel-config-shade-2.3.1.jar
+seatunnel-jackson-2.3.1-optional.jar
 slf4j-api-1.7.25.jar
 snakeyaml-1.29.jar
 spring-aop-5.3.20.jar