From c2b9c91ffc013a2bd9bc749e1dc5f0040ae564a5 Mon Sep 17 00:00:00 2001 From: gaojun Date: Sat, 6 May 2023 16:57:06 +0800 Subject: [PATCH] Add seatunnel dynamicform module --- pom.xml | 77 +++- seatunnel-server/pom.xml | 1 + .../src/main/resources/application.yml | 4 +- .../seatunnel-dynamicform/pom.xml | 57 +++ .../app/dynamicforms/AbstractFormOption.java | 118 +++++++ .../AbstractFormSelectOption.java | 49 +++ .../seatunnel/app/dynamicforms/Constants.java | 8 + .../app/dynamicforms/DynamicSelectOption.java | 33 ++ .../app/dynamicforms/FormInputOption.java | 56 +++ .../app/dynamicforms/FormLocale.java | 51 +++ .../app/dynamicforms/FormOptionBuilder.java | 165 +++++++++ .../app/dynamicforms/FormStructure.java | 65 ++++ .../dynamicforms/FormStructureBuilder.java | 74 ++++ .../dynamicforms/FormStructureValidate.java | 273 +++++++++++++++ .../app/dynamicforms/StaticSelectOption.java | 38 ++ .../FormStructureValidateException.java | 35 ++ .../validate/AbstractValidate.java | 65 ++++ .../validate/MutuallyExclusiveValidate.java | 25 ++ .../validate/NonEmptyValidate.java | 30 ++ .../validate/UnionNonEmptyValidate.java | 45 +++ .../validate/ValidateBuilder.java | 78 +++++ .../FormStructureBuilderTest.java | 329 ++++++++++++++++++ .../seatunnel/app/dynamicforms/TestUtils.java | 17 + .../src/test/resources/test_form.json | 1 + 24 files changed, 1690 insertions(+), 4 deletions(-) create mode 100644 seatunnel-server/seatunnel-dynamicform/pom.xml create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormOption.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/AbstractFormSelectOption.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/Constants.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/DynamicSelectOption.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormInputOption.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormLocale.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormOptionBuilder.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructure.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilder.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/FormStructureValidate.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/StaticSelectOption.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/exception/FormStructureValidateException.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/AbstractValidate.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/MutuallyExclusiveValidate.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/NonEmptyValidate.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/UnionNonEmptyValidate.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/ValidateBuilder.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/FormStructureBuilderTest.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/TestUtils.java create mode 100644 seatunnel-server/seatunnel-dynamicform/src/test/resources/test_form.json diff --git a/pom.xml b/pom.xml index e5195ce02..6f239f8c3 100644 --- a/pom.xml +++ b/pom.xml @@ -100,7 +100,6 @@ false - 2.1.3 1.2 1.7.25 2.12.7.1 @@ -113,6 +112,7 @@ 3.10.0 2.17.1 4.2.0 + 2.3.1-SNAPSHOT @@ -120,7 +120,80 @@ org.apache.seatunnel seatunnel-common - ${seatunnel-common.version} + ${seatunnel-framework.version} + + + + + org.apache.seatunnel + seatunnel-common + ${seatunnel-framework.version} + + + + org.apache.seatunnel + seatunnel-jackson + ${seatunnel-framework.version} + optional + + + + org.apache.seatunnel + seatunnel-api + ${seatunnel-framework.version} + + + + org.apache.seatunnel + seatunnel-engine-client + ${seatunnel-framework.version} + + + + com.google.auto.service + auto-service + ${auto-service.version} + + + + org.apache.seatunnel + seatunnel-plugin-discovery + ${seatunnel-framework.version} + + + + org.apache.seatunnel + seatunnel-transforms-v2 + ${seatunnel-framework.version} + + + + + org.apache.seatunnel + connector-common + ${seatunnel-framework.version} + test + + + + org.apache.seatunnel + connector-console + ${seatunnel-framework.version} + test + + + + org.apache.seatunnel + connector-fake + ${seatunnel-framework.version} + test + + + + org.apache.seatunnel + connector-jdbc + ${seatunnel-framework.version} + test diff --git a/seatunnel-server/pom.xml b/seatunnel-server/pom.xml index 79ef3a310..d6bd26dfc 100644 --- a/seatunnel-server/pom.xml +++ b/seatunnel-server/pom.xml @@ -27,6 +27,7 @@ pom seatunnel-app + seatunnel-dynamicform seatunnel-spi seatunnel-scheduler seatunnel-server-common diff --git a/seatunnel-server/seatunnel-app/src/main/resources/application.yml b/seatunnel-server/seatunnel-app/src/main/resources/application.yml index 77e35480e..5c6b510fd 100644 --- a/seatunnel-server/seatunnel-app/src/main/resources/application.yml +++ b/seatunnel-server/seatunnel-app/src/main/resources/application.yml @@ -25,7 +25,7 @@ spring: date-format: yyyy-MM-dd HH:mm:ss datasource: driver-class-name: com.mysql.jdbc.Driver - url: jdbc:mysql://127.0.0.1:3306/seatunnel?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true + url: jdbc:mysql://localhost:3306/seatunnel?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&allowPublicKeyRetrieval=true username: root password: 123456 mvc: @@ -40,7 +40,7 @@ ds: default: default api: token: 12345678 - prefix: http://127.0.0.1:12345/dolphinscheduler + prefix: http://localhost:12345/dolphinscheduler jwt: expireTime: 86400 diff --git a/seatunnel-server/seatunnel-dynamicform/pom.xml b/seatunnel-server/seatunnel-dynamicform/pom.xml new file mode 100644 index 000000000..36376f75d --- /dev/null +++ b/seatunnel-server/seatunnel-dynamicform/pom.xml @@ -0,0 +1,57 @@ + + + + 4.0.0 + + seatunnel-server + org.apache.seatunnel + 1.0.0-SNAPSHOT + + + seatunnel-dynamicform + + + + com.google.guava + guava + + + + org.apache.commons + commons-lang3 + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.apache.seatunnel + seatunnel-common + + + org.apache.seatunnel + seatunnel-jackson + optional + + + 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 000000000..4630bbb37 --- /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 { + + // 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 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 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 000000000..52b0c534c --- /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 000000000..bfa45ffbb --- /dev/null +++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/Constants.java @@ -0,0 +1,8 @@ +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 000000000..d1365cd67 --- /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 000000000..f71d64ac9 --- /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 000000000..466b03c6c --- /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 zhCN = new LinkedHashMap<>(); + + @JsonProperty("en_US") + private LinkedHashMap 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 000000000..8c645abf8 --- /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 options = new ArrayList<>(); + + public StaticSelectOptionBuilder(@NonNull String label, @NonNull String field) { + this.label = label; + this.field = field; + } + + public StaticSelectOptionBuilder addSelectOptions( + @NonNull List selectOptions) { + for (ImmutablePair option : selectOptions) { + options.add( + new AbstractFormSelectOption.SelectOption( + option.left.toString(), option.right.toString())); + } + return this; + } + + public StaticSelectOptionBuilder addI18nSelectOptions( + @NonNull List 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 000000000..6096b5969 --- /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> apis; + + private List forms; + + public FormStructure() {} + + public FormStructure( + @NonNull String name, + @NonNull List formOptionList, + FormLocale locale, + Map> 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 000000000..ba863680e --- /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 forms = new ArrayList<>(); + + private FormLocale locales; + + private Map> 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 000000000..025fe313b --- /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 apiErrorList = validateApiOption(formStructure); + List localeErrorList = validateLocaleOption(formStructure); + List showErrorList = validateShow(formStructure); + List unionNonErrorList = validateUnionNonEmpty(formStructure); + List 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 validateApiOption(@NonNull FormStructure formStructure) { + List errorMessageList = new ArrayList(); + Map> 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 validateLocaleOption(@NonNull FormStructure formStructure) { + List 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 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 validateShow(@NonNull FormStructure formStructure) { + List errorMessageList = new ArrayList(); + // Find all select options + List 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 validateUnionNonEmpty(@NonNull FormStructure formStructure) { + List errorMessageList = new ArrayList(); + Map> 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 validateMutuallyExclusive(@NonNull FormStructure formStructure) { + List errorMessageList = new ArrayList(); + Map> 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 000000000..e70f4a24c --- /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 options = new ArrayList<>(); + + public StaticSelectOption( + @NonNull List 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 000000000..02804e32d --- /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 errorList, @NonNull Throwable e) { + super(String.format("Form: %s, validate error - %s", formName, errorList), e); + } + + public FormStructureValidateException( + @NonNull String formName, @NonNull List 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 000000000..f5576611c --- /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 { + private final List 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 000000000..fd375d115 --- /dev/null +++ b/seatunnel-server/seatunnel-dynamicform/src/main/java/org/apache/seatunnel/app/dynamicforms/validate/MutuallyExclusiveValidate.java @@ -0,0 +1,25 @@ +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 fields; + + @JsonProperty("type") + private final RequiredType requiredType = RequiredType.MUTUALLY_EXCLUSIVE; + + public MutuallyExclusiveValidate(@NonNull List 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 000000000..0ee27cbb5 --- /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 000000000..8328459d9 --- /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 fields; + + @JsonProperty("type") + private final RequiredType requiredType = RequiredType.UNION_NON_EMPTY; + + public UnionNonEmptyValidate(@NonNull List 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 000000000..53a6e94f6 --- /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 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 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 000000000..cbea72a85 --- /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 000000000..d6cf37d8c --- /dev/null +++ b/seatunnel-server/seatunnel-dynamicform/src/test/java/org/apache/seatunnel/app/dynamicforms/TestUtils.java @@ -0,0 +1,17 @@ +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 000000000..8044981b7 --- /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","validate":{"trigger":["input","blur"],"message":"i18n.name_password_union_required","required":false,"fields":["username","password"],"type":"union-non-empty"},"inputType":"text","type":"input"},{"label":"password","field":"password","defaultValue":null,"description":"password","clearable":false,"show":{"field":"checkType","value":["nameAndPassword"]},"placeholder":"password","validate":{"trigger":["input","blur"],"message":"i18n.name_password_union_required","required":false,"fields":["username","password"],"type":"union-non-empty"},"inputType":"password","type":"input"},{"label":"content","field":"context","defaultValue":null,"description":"content","clearable":true,"placeholder":"","inputType":"textarea","type":"input"},{"label":"checkType","field":"checkType","defaultValue":"no","description":"check type","clearable":true,"placeholder":"","validate":{"trigger":["input","blur"],"message":"required","required":true,"type":"non-empty"},"options":[{"label":"no","value":"no"},{"label":"nameAndPassword","value":"nameAndPassword"}],"type":"select"},{"label":"city","field":"city","defaultValue":null,"description":"city","clearable":false,"placeholder":"","validate":{"trigger":["input","blur"],"message":"required","required":true,"type":"non-empty"},"api":"getCity","type":"select"},{"label":"exclusive1","field":"exclusive1","defaultValue":null,"description":"","clearable":false,"placeholder":"","validate":{"trigger":["input","blur"],"message":"parameters:[exclusive1, exclusive2], only one can be set","required":false,"fields":["exclusive1","exclusive2"],"type":"mutually-exclusive"},"inputType":"text","type":"input"},{"label":"exclusive2","field":"exclusive2","defaultValue":null,"description":"","clearable":false,"placeholder":"","validate":{"trigger":["input","blur"],"message":"parameters:[exclusive1, exclusive2], only one can be set","required":false,"fields":["exclusive1","exclusive2"],"type":"mutually-exclusive"},"inputType":"text","type":"input"}]} \ No newline at end of file