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); + } +}
