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

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

commit 450c62496120a72ff4fbbfa60ed1154dfe713648
Author: gaojun <[email protected]>
AuthorDate: Wed Nov 16 17:35:50 2022 +0800

    add seatunnel dynamic form
---
 pom.xml                                            |  10 +-
 seatunnel-main-repository                          |   2 +-
 seatunnel-server/pom.xml                           |   1 +
 .../thirdparty/framework/OptionRuleWrapper.java    |   8 +
 seatunnel-server/seatunnel-dynamicform/pom.xml     |  45 ++++++
 .../seatunnel/dynamicforms/AbstractFormOption.java |  99 ++++++++++++
 .../dynamicforms/AbstractFormSelectOption.java     |  31 ++++
 .../dynamicforms/DynamicSelectOption.java          |  16 ++
 .../seatunnel/dynamicforms/FormInputOption.java    |  37 +++++
 .../seatunnel/dynamicforms/FormOptionBuilder.java  | 114 ++++++++++++++
 .../seatunnel/dynamicforms/FormStructure.java      |  46 ++++++
 .../dynamicforms/FormStructureBuilder.java         |  56 +++++++
 .../dynamicforms/FormStructureValidate.java        | 156 +++++++++++++++++++
 .../org/apache/seatunnel/dynamicforms/Locale.java  |  34 ++++
 .../seatunnel/dynamicforms/StaticSelectOption.java |  20 +++
 .../exception/FormStructureValidateException.java  |  16 ++
 .../dynamicforms/validate/AbstractValidate.java    |  44 ++++++
 .../dynamicforms/validate/NonEmptyValidate.java    |  11 ++
 .../validate/UnionNonEmptyValidate.java            |  19 +++
 .../dynamicforms/validate/ValidateBuilder.java     |  42 +++++
 .../dynamicforms/FormStructureBuilderTest.java     | 172 +++++++++++++++++++++
 21 files changed, 976 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index 1a836889..50331ffb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,7 +99,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.6</jackson.version>
@@ -111,6 +110,7 @@
         <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.1.3-SNAPSHOT</seatunnel-framework.version>
     </properties>
 
     <dependencyManagement>
@@ -118,7 +118,13 @@
             <dependency>
                 <groupId>org.apache.seatunnel</groupId>
                 <artifactId>seatunnel-common</artifactId>
-                <version>${seatunnel-common.version}</version>
+                <version>${seatunnel-framework.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.seatunnel</groupId>
+                <artifactId>seatunnel-api</artifactId>
+                <version>${seatunnel-framework.version}</version>
             </dependency>
 
             <dependency>
diff --git a/seatunnel-main-repository b/seatunnel-main-repository
index b85317bc..9781a6a3 160000
--- a/seatunnel-main-repository
+++ b/seatunnel-main-repository
@@ -1 +1 @@
-Subproject commit b85317bcbe080d6278030a8a5d0206995df50b30
+Subproject commit 9781a6a385d6229dbca617b8cdf83a75e599c715
diff --git a/seatunnel-server/pom.xml b/seatunnel-server/pom.xml
index 572a165c..6c08c89d 100644
--- a/seatunnel-server/pom.xml
+++ b/seatunnel-server/pom.xml
@@ -30,6 +30,7 @@
         <module>seatunnel-spi</module>
         <module>seatunnel-scheduler</module>
         <module>seatunnel-server-common</module>
+        <module>seatunnel-dynamicform</module>
     </modules>
 
     <properties>
diff --git 
a/seatunnel-server/seatunnel-app/src/main/java/org/apache/seatunnel/app/thirdparty/framework/OptionRuleWrapper.java
 
b/seatunnel-server/seatunnel-app/src/main/java/org/apache/seatunnel/app/thirdparty/framework/OptionRuleWrapper.java
new file mode 100644
index 00000000..d4baf015
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-app/src/main/java/org/apache/seatunnel/app/thirdparty/framework/OptionRuleWrapper.java
@@ -0,0 +1,8 @@
+package org.apache.seatunnel.app.thirdparty.framework;
+
+/**
+ * This class is used to wrapper the seatunnel option rules to seatunnel web 
form information
+ */
+public class OptionRuleWrapper {
+
+}
diff --git a/seatunnel-server/seatunnel-dynamicform/pom.xml 
b/seatunnel-server/seatunnel-dynamicform/pom.xml
new file mode 100644
index 00000000..cd920bfc
--- /dev/null
+++ b/seatunnel-server/seatunnel-dynamicform/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<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";>
+    <parent>
+        <artifactId>seatunnel-server</artifactId>
+        <groupId>org.apache.seatunnel</groupId>
+        <version>1.0.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>seatunnel-dynamicform</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </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>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormOption.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormOption.java
new file mode 100644
index 00000000..ed56c1a7
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormOption.java
@@ -0,0 +1,99 @@
+package org.apache.seatunnel.dynamicforms;
+
+import org.apache.seatunnel.dynamicforms.validate.AbstractValidate;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.HashMap;
+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 String 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 Object value) {
+        if (this.show == null) {
+            this.show = new HashMap<>();
+        }
+
+        this.show.put("field", field);
+        this.show.put("value", value);
+        return (T) this;
+    }
+
+    public T withValidate(@NonNull V validate) {
+        this.validate = validate;
+        return (T) this;
+    }
+
+    public T withDefaultValue(@NonNull String 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 = Locale.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 = Locale.I18N_PREFIX + placeholder;
+        return (T) this;
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormSelectOption.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormSelectOption.java
new file mode 100644
index 00000000..a1b1231e
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/AbstractFormSelectOption.java
@@ -0,0 +1,31 @@
+package org.apache.seatunnel.dynamicforms;
+
+import 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/dynamicforms/DynamicSelectOption.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/DynamicSelectOption.java
new file mode 100644
index 00000000..ccbbb8d0
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/DynamicSelectOption.java
@@ -0,0 +1,16 @@
+package org.apache.seatunnel.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/dynamicforms/FormInputOption.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormInputOption.java
new file mode 100644
index 00000000..926f9849
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormInputOption.java
@@ -0,0 +1,37 @@
+package org.apache.seatunnel.dynamicforms;
+
+import 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/dynamicforms/FormOptionBuilder.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormOptionBuilder.java
new file mode 100644
index 00000000..391c4028
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormOptionBuilder.java
@@ -0,0 +1,114 @@
+package org.apache.seatunnel.dynamicforms;
+
+import lombok.NonNull;
+
+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 = Locale.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 AbstractFormSelectOption.SelectOption... selectOptions) {
+            for (AbstractFormSelectOption.SelectOption option : selectOptions) 
{
+                options.add(option);
+            }
+            return this;
+        }
+
+        public StaticSelectOption formStaticSelectOption() {
+            return new StaticSelectOption(options, label, field);
+        }
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructure.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructure.java
new file mode 100644
index 00000000..910ff78a
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructure.java
@@ -0,0 +1,46 @@
+package org.apache.seatunnel.dynamicforms;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.google.common.base.Preconditions;
+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 Locale locales;
+
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private Map<String, Map<String, String>> apis;
+
+    private final List<AbstractFormOption> forms;
+
+    public FormStructure(@NonNull String name, @NonNull 
List<AbstractFormOption> formOptionList, Locale locale,
+                         Map<String, Map<String, String>> apis) {
+        Preconditions.checkArgument(formOptionList.size() > 1);
+        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/dynamicforms/FormStructureBuilder.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructureBuilder.java
new file mode 100644
index 00000000..ac8bdd5f
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructureBuilder.java
@@ -0,0 +1,56 @@
+package org.apache.seatunnel.dynamicforms;
+
+import 
org.apache.seatunnel.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 Locale 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(@NonNull Locale 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/dynamicforms/FormStructureValidate.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructureValidate.java
new file mode 100644
index 00000000..1cd03e6c
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/FormStructureValidate.java
@@ -0,0 +1,156 @@
+package org.apache.seatunnel.dynamicforms;
+
+import 
org.apache.seatunnel.dynamicforms.exception.FormStructureValidateException;
+import org.apache.seatunnel.dynamicforms.validate.AbstractValidate;
+import org.apache.seatunnel.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:
+     * <li>All data_integration.xxx need found xxx in locales</li>
+     * <li>All api used in select option need found in apis</li>
+     *
+     * @param formStructure
+     * @throws FormStructureValidateException
+     */
+    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);
+
+        apiErrorList.addAll(localeErrorList);
+        apiErrorList.addAll(showErrorList);
+        apiErrorList.addAll(unionNonErrorList);
+
+        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();
+        Locale locales = formStructure.getLocales();
+        formStructure.getForms().forEach(formOption -> {
+            if (formOption.getLabel().startsWith(Locale.I18N_PREFIX)) {
+                String labelName = 
formOption.getLabel().replace(Locale.I18N_PREFIX, "");
+                validateOneI18nOption(locales, formOption.getLabel(), "label", 
labelName, errorMessageList);
+            }
+
+            if (formOption.getDescription().startsWith(Locale.I18N_PREFIX)) {
+                String description = 
formOption.getDescription().replace(Locale.I18N_PREFIX, "");
+                validateOneI18nOption(locales, formOption.getLabel(), 
"description", description, errorMessageList);
+            }
+
+            if (formOption.getPlaceholder().startsWith(Locale.I18N_PREFIX)) {
+                String placeholder = 
formOption.getPlaceholder().replace(Locale.I18N_PREFIX, "");
+                validateOneI18nOption(locales, formOption.getLabel(), 
"placeholder", placeholder, errorMessageList);
+            }
+
+            AbstractValidate validate = formOption.getValidate();
+            if (validate != null && 
validate.getMessage().startsWith(Locale.I18N_PREFIX)) {
+                String message = 
validate.getMessage().replace(Locale.I18N_PREFIX, "");
+                validateOneI18nOption(locales, formOption.getLabel(), 
"validateMessage", message, errorMessageList);
+            }
+        });
+        return errorMessageList;
+    }
+
+    private static void validateOneI18nOption(Locale 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> selectFields =
+            formStructure.getForms().stream().filter(formOption -> formOption 
instanceof AbstractFormSelectOption)
+                .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 (selectFields == null || !selectFields.contains(field)) {
+                errorMessageList.add(String.format("FormOption[%s] used show 
field[%s] can not found in select 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;
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/Locale.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/Locale.java
new file mode 100644
index 00000000..23aa0ea1
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/Locale.java
@@ -0,0 +1,34 @@
+package org.apache.seatunnel.dynamicforms;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NonNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Multi language support
+ */
+@Data
+public class Locale {
+    @JsonIgnore
+    public static final String I18N_PREFIX = "i18n.";
+
+    @JsonProperty("zh_CN")
+    private Map<String, String> zhCN = new HashMap<>();
+
+    @JsonProperty("en_US")
+    private Map<String, String> enUS = new HashMap<>();
+
+    public Locale addZhCN(@NonNull String key, @NonNull String value) {
+        zhCN.put(key, value);
+        return this;
+    }
+
+    public Locale 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/dynamicforms/StaticSelectOption.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/StaticSelectOption.java
new file mode 100644
index 00000000..b19b27a6
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/StaticSelectOption.java
@@ -0,0 +1,20 @@
+package org.apache.seatunnel.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/dynamicforms/exception/FormStructureValidateException.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/exception/FormStructureValidateException.java
new file mode 100644
index 00000000..f751ddf2
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/exception/FormStructureValidateException.java
@@ -0,0 +1,16 @@
+package org.apache.seatunnel.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/dynamicforms/validate/AbstractValidate.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/AbstractValidate.java
new file mode 100644
index 00000000..ea42a663
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/AbstractValidate.java
@@ -0,0 +1,44 @@
+package org.apache.seatunnel.dynamicforms.validate;
+
+import org.apache.seatunnel.dynamicforms.Locale;
+
+import 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");
+
+        @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 = Locale.I18N_PREFIX + message;
+        return (T) this;
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/NonEmptyValidate.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/NonEmptyValidate.java
new file mode 100644
index 00000000..de4acce8
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/NonEmptyValidate.java
@@ -0,0 +1,11 @@
+package org.apache.seatunnel.dynamicforms.validate;
+
+import 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/dynamicforms/validate/UnionNonEmptyValidate.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/UnionNonEmptyValidate.java
new file mode 100644
index 00000000..13579fd5
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/UnionNonEmptyValidate.java
@@ -0,0 +1,19 @@
+package org.apache.seatunnel.dynamicforms.validate;
+
+import com.google.common.base.Preconditions;
+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;
+    private final RequiredType requiredType = RequiredType.UNION_NON_EMPTY;
+
+    public UnionNonEmptyValidate(@NonNull List<String> fields) {
+        Preconditions.checkArgument(fields.size() > 0);
+        this.fields = fields;
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/ValidateBuilder.java
 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/ValidateBuilder.java
new file mode 100644
index 00000000..57511c7b
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/dynamicforms/validate/ValidateBuilder.java
@@ -0,0 +1,42 @@
+package org.apache.seatunnel.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 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);
+        }
+    }
+}
diff --git 
a/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/dynamicforms/FormStructureBuilderTest.java
 
b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/dynamicforms/FormStructureBuilderTest.java
new file mode 100644
index 00000000..668a800f
--- /dev/null
+++ 
b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/dynamicforms/FormStructureBuilderTest.java
@@ -0,0 +1,172 @@
+package org.apache.seatunnel.dynamicforms;
+
+import 
org.apache.seatunnel.dynamicforms.exception.FormStructureValidateException;
+import org.apache.seatunnel.dynamicforms.validate.ValidateBuilder;
+import org.apache.seatunnel.common.utils.JsonUtils;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class FormStructureBuilderTest {
+
+    @Test
+    public void testFormStructureBuild() {
+        Locale locale = new Locale();
+        locale.addZhCN("name_password_union_required", "用户名和密码都不能为空")
+            .addZhCN("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", "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", "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 AbstractFormSelectOption.SelectOption("no", 
"no"),
+                new AbstractFormSelectOption.SelectOption("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());
+
+        FormStructure testForm = FormStructure.builder()
+            .name("testForm")
+            .addFormOption(nameOption, passwordOption, textAreaOption, 
checkTypeOption, cityOption)
+            .withLocale(locale)
+            .addApi("getCity", "/api/get_city", FormStructure.HttpMethod.GET)
+            .build();
+
+        String s = JsonUtils.toJsonString(testForm);
+        String result =
+            
"{\"name\":\"testForm\",\"locales\":{\"zh_CN\":{\"name_password_union_required\":\"用户名和密码都不能为空\",\"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\":\"n
 [...]
+        Assertions.assertEquals(result, s);
+    }
+
+    @Test
+    public void testFormStructureValidate() {
+        Locale locale = new Locale();
+        locale.addZhCN("name_password_union_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", "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", "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 AbstractFormSelectOption.SelectOption("no", 
"no"),
+                new AbstractFormSelectOption.SelectOption("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());
+
+        String error = "";
+        try {
+            FormStructure testForm = FormStructure.builder()
+                .name("testForm")
+                .addFormOption(nameOption, passwordOption, textAreaOption, 
checkTypeOption, cityOption)
+                .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 select 
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]";
+        Assertions.assertEquals(result, error);
+    }
+}


Reply via email to