├── gradle.properties ├── settings.gradle.kts ├── gradle └── wrapper │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── com │ │ └── baidu │ │ └── amis │ │ ├── util │ │ ├── JSONFind.java │ │ ├── Script.java │ │ └── JSONHelper.java │ │ └── validation │ │ ├── ValidatorFlag.java │ │ ├── ConstraintViolation.java │ │ ├── ViolationMessage.java │ │ ├── ValidationFn.java │ │ └── Validator.java └── test │ └── kotlin │ └── com │ └── baidu │ └── amis │ ├── util │ └── JSONHelperTest.kt │ └── validation │ ├── ValidationFnTest.kt │ └── ValidatorTest.kt ├── .gitignore ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "amis-server" 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/util/JSONFind.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.util; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | 5 | @FunctionalInterface 6 | public interface JSONFind { 7 | /** 8 | * 在 JSON 查找的 lambda 9 | * @param node 当前节点 10 | * @param parentNode 父级节点 11 | * @return 是否找到 12 | */ 13 | boolean find(String key, JsonNode node, JsonNode parentNode); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/validation/ValidatorFlag.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | import java.util.EnumSet; 4 | 5 | /** 6 | * 校验的功能开关,目前只有一个 7 | */ 8 | public enum ValidatorFlag { 9 | /** 10 | * 是否关闭脚本功能,这个功能是用来支持类似 requireOn 这样的属性,但可能会不准确,主要有两方面: 11 | * 1. 在数据属于上级数据域,后端是拿不到的,这个问题无解 12 | * 2. java 中的 nashorn 引擎只支持 es5,但前端是可以支持 es6 的,要解决必须使用 GraalVM 最新版本,对部署要求比较高 13 | */ 14 | DISABLE_SCRIPT; 15 | 16 | public static final EnumSet ALL_OPTS = EnumSet.allOf(ValidatorFlag.class); 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Ignore Gradle project-specific cache directory 26 | .gradle 27 | 28 | .idea 29 | 30 | # Ignore Gradle build output directory 31 | build 32 | 33 | target 34 | 35 | .DS_Store -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/validation/ConstraintViolation.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | /** 4 | * 仿照 javax.validation 力度命名,但多了个字段名 5 | */ 6 | 7 | public class ConstraintViolation { 8 | // 字段名 9 | private String name; 10 | 11 | // 违反信息 12 | private String message; 13 | 14 | public ConstraintViolation(String name, String message) { 15 | this.name = name; 16 | this.message = message; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public String getMessage() { 24 | return message; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/util/Script.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.util; 2 | 3 | import java.util.logging.Logger; 4 | 5 | import javax.script.ScriptEngine; 6 | import javax.script.ScriptEngineManager; 7 | import javax.script.ScriptException; 8 | import javax.script.SimpleBindings; 9 | 10 | /** 11 | * 执行 amis 中的脚本判断 12 | */ 13 | public class Script { 14 | 15 | private static Logger logger = Logger.getLogger("Validator"); 16 | 17 | public static boolean eval(String script, SimpleBindings dataBindings) { 18 | ScriptEngineManager manager = new ScriptEngineManager(); 19 | ScriptEngine engine = manager.getEngineByName("nashorn"); // TODO: 后续支持 GraalVM 20 | 21 | try { 22 | Boolean res = (Boolean) engine.eval(script, dataBindings); 23 | return res; 24 | } catch (ScriptException e) { 25 | logger.warning(e.getMessage()); 26 | } 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amis 后端功能辅助 2 | 3 | 目前实现了: 4 | 5 | * amis 表单验证,用于实现和 amis 同样的后端验证,避免重复开发 6 | 7 | ## 使用方法 8 | 9 | 目前还没发布到 maven center,所以暂时需要通过拷贝源码的方式使用 10 | 11 | ```java 12 | import com.baidu.amis.validation.Validator; 13 | 14 | // 第一个参数是 amis schema,第二个参数是表单 name,第三个参数是提交数据的 json 15 | // 为了方便查看,下面示例使用 java 15 的 text block 16 | List violations=Validator.validate(""" 17 | { 18 | "type": "page", 19 | "body": { 20 | "type": "form", 21 | "api": "/api/mock2/form/saveForm", 22 | "name": "myForm", 23 | "body": [ 24 | { 25 | "type": "input-text", 26 | "label": "文本", 27 | "name": "text", 28 | "validations": { 29 | "isNumeric": true 30 | }, 31 | "validationErrors": { 32 | "isNumeric": "同学,请输入数字哈" 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | ""","myForm",""" 39 | { 40 | "text": "v" 41 | } 42 | """); 43 | 44 | // 如果 violations 的结果应该是 45 | violations[0].getMessage()=="同学,请输入数字哈" 46 | ``` 47 | 48 | ## 开发 49 | 50 | 目前主要以库的形式对外提供,但还没有 maven 仓库,只能先拷贝代码使用 51 | 52 | ## 测试 53 | 54 | 因为需要构造大量 JSON 进行测试,而 Java 在 15 之前不支持多行字符串,构造起来麻烦,所以测试的代码是基于 kotlin,可以直接在 IDE 里运行单个测试,或者使用下面的命令运行所有测试: 55 | 56 | ```shell 57 | ./gradlew check 58 | ``` 59 | 60 | 生成测试报告 61 | 62 | ```shell 63 | ./gradlew clean build 64 | ``` 65 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/validation/ViolationMessage.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | /** 4 | * 默认验证信息,拷贝自 amis 的 locale/zh-CN.ts 5 | */ 6 | public class ViolationMessage { 7 | static final String equals = "输入的数据与 $1 不一致"; 8 | static final String equalsField = "输入的数据与 $1 值不一致"; 9 | static final String gt = "请输入大于 $1 的值"; 10 | static final String isAlpha = "请输入字母"; 11 | static final String isAlphanumeric = "请输入字母或者数字"; 12 | static final String isEmail = "Email 格式不正确"; 13 | static final String isFloat = "请输入浮点型数值"; 14 | static final String isId = "请输入合法的身份证号"; 15 | static final String isInt = "请输入整型数字"; 16 | static final String isJson = "JSON 格式不正确"; 17 | static final String isLength = "请输入长度为 $1 的内容"; 18 | static final String isNumeric = "请输入数字"; 19 | static final String isPhoneNumber = "请输入合法的手机号码"; 20 | static final String isRequired = "这是必填项"; 21 | static final String isTelNumber = "请输入合法的电话号码"; 22 | static final String isUrl = "URL 格式不正确"; 23 | static final String isUrlPath = "只能输入字母、数字、`-` 和 `_`."; 24 | static final String isWords = "请输入单词"; 25 | static final String isZipcode = "请输入合法的邮编地址"; 26 | static final String isExisty = "不存在这个值"; 27 | static final String lt = "请输入小于 $1 的值"; 28 | static final String matchRegexp = "格式不正确, 请输入符合规则为 $1 的内容"; 29 | static final String maximum = "当前输入值超出最大值 $1"; 30 | static final String maxLength = "请控制内容长度, 不要输入 $1 个以上字符"; 31 | static final String minimum = "当前输入值低于最小值 $1"; 32 | static final String minLength = "请输入更多的内容,至少输入 $1 个字符"; 33 | static final String notEmptyString = "请不要全输入空白字符"; 34 | } 35 | -------------------------------------------------------------------------------- /src/test/kotlin/com/baidu/amis/util/JSONHelperTest.kt: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.util 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import org.junit.jupiter.api.Test 6 | import kotlin.test.assertEquals 7 | 8 | 9 | internal class JSONHelperTest { 10 | // 测试 json 查找功能 11 | @Test 12 | fun testJSONFind() { 13 | val mapper = ObjectMapper() 14 | val amisSchema: JsonNode = mapper.readTree( 15 | """ 16 | { 17 | "type": "page", 18 | "body": [{ 19 | "type": "form", 20 | "name": "myForm", 21 | "api": "https://3xsw4ap8wah59.cfc-execute.bj.baidubce.com/api/amis-mock/mock2/form/saveForm", 22 | "body": [ 23 | { 24 | "type": "input-text", 25 | "name": "name", 26 | "label": "姓名:" 27 | }, 28 | { 29 | "name": "email", 30 | "type": "input-email", 31 | "label": "邮箱:" 32 | } 33 | ] 34 | }] 35 | } 36 | """.trimIndent() 37 | ) 38 | val amisNode = JSONHelper.findObject( 39 | amisSchema 40 | ) { key, node, parentNode -> 41 | if (key != null && key.equals("name")) { 42 | val type: String = parentNode.get("type").asText() 43 | if (type == "form") { 44 | if (node.asText().equals("myForm")) { 45 | println("find") 46 | return@findObject true 47 | } 48 | } 49 | } 50 | return@findObject false 51 | } 52 | assertEquals(amisNode.get("type").asText(), "form") 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/baidu/amis/validation/ValidationFnTest.kt: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | import com.fasterxml.jackson.databind.node.NullNode 4 | import com.fasterxml.jackson.databind.node.TextNode 5 | import org.junit.jupiter.api.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class ValidationFnTest { 9 | @Test 10 | fun testIsRequired() { 11 | assertEquals(ValidationFn.isEmptyString(null), false) 12 | assertEquals(ValidationFn.isRequired(TextNode("a")), true) 13 | assertEquals(ValidationFn.isRequired(TextNode("")), false) 14 | assertEquals(ValidationFn.isRequired(NullNode.getInstance()), false) 15 | } 16 | 17 | @Test 18 | fun testIsExisty() { 19 | assertEquals(ValidationFn.isEmptyString(null), false) 20 | assertEquals(ValidationFn.isExisty(TextNode("")), true) 21 | assertEquals(ValidationFn.isExisty(NullNode.getInstance()), false) 22 | } 23 | 24 | @Test 25 | fun testIsEmptyString() { 26 | assertEquals(ValidationFn.isEmptyString(null), false) 27 | assertEquals(ValidationFn.isEmptyString(TextNode("a")), false) 28 | assertEquals(ValidationFn.isEmptyString(TextNode("")), true) 29 | } 30 | 31 | @Test 32 | fun testIsEmail() { 33 | assertEquals(ValidationFn.isEmptyString(null), false) 34 | assertEquals(ValidationFn.isEmail(TextNode("aa@bb.com")), true) 35 | assertEquals(ValidationFn.isEmail(TextNode("aabb.com")), false) 36 | } 37 | 38 | @Test 39 | fun testIsSpecialWords() { 40 | assertEquals(ValidationFn.isSpecialWords(TextNode("ab")), true) 41 | assertEquals(ValidationFn.isSpecialWords(TextNode("aabbÂÄBC")), true) 42 | assertEquals(ValidationFn.isSpecialWords(TextNode("中文")), false) 43 | } 44 | 45 | @Test 46 | fun testMaximum() { 47 | assertEquals(ValidationFn.maximum(TextNode("10"), 20.0), true) 48 | assertEquals(ValidationFn.maximum(TextNode("30"), 20.0), false) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/util/JSONHelper.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.Map; 5 | 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.databind.JsonNode; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | 11 | /** 12 | * amis 中有大量 JSON 操作,这个类主要用于 13 | */ 14 | public class JSONHelper { 15 | /** 16 | * 在 json 节点中查找对象 17 | * @param node JSON 节点 18 | * @param finder 查找 lambda 19 | * @return 20 | */ 21 | public static JsonNode findObject(JsonNode node, JSONFind finder) { 22 | return findObject(node, finder, null , "", 0); 23 | } 24 | 25 | /** 26 | * 将字符串转成 JSON 节点 27 | * @param str 28 | * @return 29 | * @throws JsonProcessingException 30 | */ 31 | public static JsonNode toJSONNode(String str) throws JsonProcessingException { 32 | ObjectMapper mapper = new ObjectMapper(); 33 | mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); 34 | mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 35 | mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true); 36 | JsonNode amisSchema = mapper.readTree(str); 37 | return amisSchema; 38 | } 39 | 40 | // 查找 JSON 对象的内部实现 41 | private static JsonNode findObject(JsonNode node, JSONFind finder, JsonNode parent, String key, int index) { 42 | if (node.isObject()) { 43 | Iterator> it = node.fields(); 44 | while (it.hasNext()) { 45 | Map.Entry entry = it.next(); 46 | JsonNode result = findObject(entry.getValue(), finder, node, entry.getKey(), 0); 47 | if (result != null) { 48 | return result; 49 | } 50 | } 51 | } else if (node.isArray()) { 52 | Iterator it = node.iterator(); 53 | int i = 0; 54 | while (it.hasNext()) { 55 | JsonNode result = findObject(it.next(), finder, node, "[]", i); 56 | if (result != null) { 57 | return result; 58 | } 59 | i = i + 1; 60 | } 61 | } else if (node.isValueNode()) { 62 | if (parent != null) { 63 | if (parent.isObject()) { 64 | if (finder.find(key, node, parent)) { 65 | return parent; 66 | } 67 | } 68 | } 69 | } 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/validation/ValidationFn.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | import java.io.IOException; 4 | import java.util.regex.Pattern; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.apache.commons.lang3.math.NumberUtils; 8 | import org.apache.commons.validator.routines.EmailValidator; 9 | import org.apache.commons.validator.routines.UrlValidator; 10 | 11 | import com.fasterxml.jackson.databind.JsonNode; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import com.fasterxml.jackson.databind.node.JsonNodeType; 14 | 15 | /** 16 | * 实现 amis 中 src/utils/validations 里的校验方法 17 | * 但实现不完全一致,有些正则判断改成了 commons 里的方法来方便维护,可能会造成前后端校验不一致 18 | * 目前还不清楚 js 和 java 在正则上有哪些不一致,这会导致自定义正则可能会不一致 19 | */ 20 | public class ValidationFn { 21 | /** 22 | * 必填项是否有值 23 | */ 24 | static boolean isRequired(JsonNode value) { 25 | if (value == null) { 26 | return false; 27 | } 28 | JsonNodeType nodeType = value.getNodeType(); 29 | switch (nodeType) { 30 | case NULL: 31 | case MISSING: 32 | return false; 33 | case ARRAY: 34 | return !value.isEmpty(); 35 | case STRING: 36 | return !value.asText().equals(""); 37 | } 38 | return true; 39 | } 40 | 41 | /** 42 | * 是否存在这个值,只要不是 null 就是存在 43 | */ 44 | static boolean isExisty(JsonNode value) { 45 | if (value == null) { 46 | return false; 47 | } 48 | JsonNodeType nodeType = value.getNodeType(); 49 | switch (nodeType) { 50 | case NULL: 51 | case MISSING: 52 | return false; 53 | } 54 | return true; 55 | } 56 | 57 | /** 58 | * 判断字符串是否符合正则,主要是将检查的正则字符串转成正则对象后调用 {@link #matchRegexp(JsonNode value, Pattern pattern)} 59 | * 60 | * @param regex 正则字符串 61 | */ 62 | static boolean matchRegexp(JsonNode value, String regex) { 63 | return matchRegexp(value, Pattern.compile(regex)); 64 | } 65 | 66 | /** 67 | * 根据正则进行判断,大部分方法是基于这个 68 | * 69 | * @param pattern 正则 70 | */ 71 | static boolean matchRegexp(JsonNode value, Pattern pattern) { 72 | if (value == null) { 73 | return false; 74 | } 75 | String text = value.asText(); 76 | if (value.asText().equals("")) { 77 | return false; 78 | } 79 | return pattern.matcher(text).find(); 80 | } 81 | 82 | /** 83 | * 是否是 undefined,它的值永远为 false,这是 amis 前端才需要的,转成 json 后就不会有 undefined 的数据 84 | * 85 | * @return 永远是 false 86 | */ 87 | static boolean isUndefined(JsonNode value) { 88 | return false; 89 | } 90 | 91 | /** 92 | * 是否是空字符串 93 | */ 94 | static boolean isEmptyString(JsonNode value) { 95 | return isString(value) && value.asText().equals(""); 96 | } 97 | 98 | // 内部方法,主要是很多基于正则的判断是需要确保字符串的 99 | static private boolean isString(JsonNode value) { 100 | return value != null && value.getNodeType() == JsonNodeType.STRING; 101 | } 102 | 103 | /** 104 | * 是否是邮箱地址 105 | */ 106 | static boolean isEmail(JsonNode value) { 107 | if (isString(value)) { 108 | return EmailValidator.getInstance().isValid(value.asText()); 109 | } 110 | return false; 111 | } 112 | 113 | /** 114 | * 是否是网址 115 | */ 116 | static boolean isUrl(JsonNode value) { 117 | if (isString(value)) { 118 | return UrlValidator.getInstance().isValid(value.asText()); 119 | } 120 | return false; 121 | } 122 | 123 | /** 124 | * 是否是 true 125 | */ 126 | static boolean isTrue(JsonNode value) { 127 | return value != null && value.getNodeType() == JsonNodeType.BOOLEAN && value.asBoolean(); 128 | } 129 | 130 | /** 131 | * 是否是 false 132 | */ 133 | static boolean isFalse(JsonNode value) { 134 | return value != null && value.getNodeType() == JsonNodeType.BOOLEAN && !value.asBoolean(); 135 | } 136 | 137 | /** 138 | * 是否是数字 139 | */ 140 | static boolean isNumeric(JsonNode value) { 141 | // 目前看来这里不需要判断类型,即便是数字,asText() 也会转成对应的字符串 142 | return value != null && NumberUtils.isCreatable(value.asText()); 143 | } 144 | 145 | /** 146 | * 是否只有英文字母 147 | */ 148 | static boolean isAlpha(JsonNode value) { 149 | return value != null && !value.asText().isEmpty() && value.asText().chars().allMatch(Character::isLetter); 150 | } 151 | 152 | /** 153 | * 是否只有英文字母和数字 154 | */ 155 | static boolean isAlphanumeric(JsonNode value) { 156 | return value != null && !value.asText().isEmpty() && value.asText().chars() 157 | .allMatch(Character::isLetterOrDigit); 158 | } 159 | 160 | /** 161 | * 是否是整数 162 | */ 163 | static boolean isInt(JsonNode value) { 164 | return matchRegexp(value, "^(?:[-+]?(?:0|[1-9]\\d*))$"); 165 | } 166 | 167 | /** 168 | * 是否是浮点数 169 | */ 170 | static boolean isFloat(JsonNode value) { 171 | return matchRegexp(value, "^(?:[-+]?(?:\\d+))?(?:\\.\\d*)?(?:[eE][\\+\\-]?(?:\\d+))?$"); 172 | } 173 | 174 | /** 175 | * 是否是字母或空格 176 | */ 177 | static boolean isWords(JsonNode value) { 178 | Pattern pattern = Pattern.compile("^[A-Z\\s]+$", Pattern.CASE_INSENSITIVE); 179 | return matchRegexp(value, pattern); 180 | } 181 | 182 | /** 183 | * 是否是字母及带重音的字母 184 | */ 185 | static boolean isSpecialWords(JsonNode value) { 186 | Pattern pattern = Pattern.compile("^[A-Z\\s\\u00C0-\\u017F]+$", Pattern.CASE_INSENSITIVE); 187 | return matchRegexp(value, pattern); 188 | } 189 | 190 | /** 191 | * 是否长度正好等于设定值 192 | */ 193 | static boolean isLength(JsonNode value, int length) { 194 | return value != null && value.asText().length() == length; 195 | } 196 | 197 | /** 198 | * 是否值等于某个值 199 | */ 200 | static boolean equals(JsonNode value, JsonNode otherValue) { 201 | return value != null && value.equals(otherValue); 202 | } 203 | 204 | /** 205 | * 是否值等于另一个字段值,注意因为这个功能只能拿到表单数据,所以无法支持 amis 的数据域查找 206 | */ 207 | static boolean equalsField(JsonNode value, String fieldName) { 208 | JsonNode otherField = value.get(fieldName); 209 | return value.equals(otherField); 210 | } 211 | 212 | /** 213 | * 最大长度 214 | */ 215 | static boolean maxLength(JsonNode value, int length) { 216 | return value != null && value.asText().length() <= length; 217 | } 218 | 219 | /** 220 | * 最小长度 221 | */ 222 | static boolean minLength(JsonNode value, int length) { 223 | return value != null && value.asText().length() > length; 224 | } 225 | 226 | /** 227 | * 是否是 url 路径 228 | */ 229 | static boolean isUrlPath(JsonNode value) { 230 | Pattern pattern = Pattern.compile("^[a-z0-9_\\\\-]+$", Pattern.CASE_INSENSITIVE); 231 | return matchRegexp(value, pattern); 232 | } 233 | 234 | /** 235 | * 最大值 236 | */ 237 | static boolean maximum(JsonNode value, Double compare) { 238 | return value != null && Double.compare(value.asDouble(), compare) < 0; 239 | } 240 | 241 | /** 242 | * 小于等于 243 | */ 244 | static boolean lt(JsonNode value, Double compare) { 245 | return value != null && Double.compare(value.asDouble(), compare) <= 0; 246 | } 247 | 248 | /** 249 | * 最小值 250 | */ 251 | static boolean minimum(JsonNode value, Double compare) { 252 | return value != null && Double.compare(value.asDouble(), compare) > 0; 253 | } 254 | 255 | /** 256 | * 大于等于 257 | */ 258 | static boolean gt(JsonNode value, Double compare) { 259 | return value != null && Double.compare(value.asDouble(), compare) >= 0; 260 | } 261 | 262 | /** 263 | * 字符串是否是 json 格式 264 | */ 265 | static boolean isJson(JsonNode value) { 266 | if (value == null) { 267 | return false; 268 | } 269 | try { 270 | final ObjectMapper mapper = new ObjectMapper(); 271 | mapper.readTree(value.asText()); 272 | return true; 273 | } catch (IOException e) { 274 | return false; 275 | } 276 | } 277 | 278 | /** 279 | * 字符串是否是手机号,这里只考虑国内的情况 280 | */ 281 | static boolean isPhoneNumber(JsonNode value) { 282 | return matchRegexp(value, "^[1]([3-9])[0-9]{9}$"); 283 | } 284 | 285 | /** 286 | * 字符串是否是电话号码,这里只考虑国内的情况 287 | */ 288 | static boolean isTelNumber(JsonNode value) { 289 | return matchRegexp(value, "^(\\(\\d{3,4}\\)|\\d{3,4}-|\\s)?\\d{7,14}$"); 290 | } 291 | 292 | /** 293 | * 是否是邮编,只考虑国内的情况 294 | */ 295 | static boolean isZipcode(JsonNode value) { 296 | return matchRegexp(value, "^[1-9]{1}(\\d+){5}$"); 297 | } 298 | 299 | /** 300 | * 是否是身份证号,这里没做有效性验证 301 | */ 302 | static boolean isId(JsonNode value) { 303 | return matchRegexp(value, "(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])" 304 | + "|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)" 305 | + "\\d{3}$)"); 306 | } 307 | 308 | /** 309 | * 不是空白字符串 310 | */ 311 | static boolean notEmptyString(JsonNode value) { 312 | return value != null && !value.asText().isEmpty() && StringUtils.isBlank(value.asText()); 313 | } 314 | 315 | /** 316 | * 是否匹配某个正则,预留了多个 317 | */ 318 | static boolean matchRegexp1(JsonNode value, String regex) { 319 | return matchRegexp(value, regex); 320 | } 321 | 322 | static boolean matchRegexp2(JsonNode value, String regex) { 323 | return matchRegexp(value, regex); 324 | } 325 | 326 | static boolean matchRegexp3(JsonNode value, String regex) { 327 | return matchRegexp(value, regex); 328 | } 329 | 330 | static boolean matchRegexp4(JsonNode value, String regex) { 331 | return matchRegexp(value, regex); 332 | } 333 | 334 | static boolean matchRegexp5(JsonNode value, String regex) { 335 | return matchRegexp(value, regex); 336 | } 337 | 338 | static boolean matchRegexp6(JsonNode value, String regex) { 339 | return matchRegexp(value, regex); 340 | } 341 | 342 | static boolean matchRegexp7(JsonNode value, String regex) { 343 | return matchRegexp(value, regex); 344 | } 345 | 346 | static boolean matchRegexp8(JsonNode value, String regex) { 347 | return matchRegexp(value, regex); 348 | } 349 | 350 | static boolean matchRegexp9(JsonNode value, String regex) { 351 | return matchRegexp(value, regex); 352 | } 353 | 354 | } 355 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/baidu/amis/validation/Validator.java: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | 9 | import javax.script.SimpleBindings; 10 | 11 | import com.baidu.amis.util.JSONHelper; 12 | import com.baidu.amis.util.Script; 13 | import com.fasterxml.jackson.core.JsonProcessingException; 14 | import com.fasterxml.jackson.databind.JsonNode; 15 | import com.fasterxml.jackson.databind.ObjectMapper; 16 | import com.fasterxml.jackson.databind.node.JsonNodeType; 17 | import com.fasterxml.jackson.databind.node.ObjectNode; 18 | 19 | /** 20 | * 用于校验数据是否符合 amis 表单 schema 中配置的表单校验规则 21 | */ 22 | public class Validator { 23 | 24 | /** 25 | * 根据解析的 JSON 节点来进行校验,需要注意第一个参数必须是表单对应的 amis 配置,如果没有解析出来推荐使用后面的方法 26 | * 27 | * @param form 相关表单的 amis JSON 配置 28 | * @param data 数据的 JSON 29 | * @return 违反规则的列表,如果列表为空意味着没有违反 30 | */ 31 | public static List validate(JsonNode form, JsonNode data) { 32 | JsonNode body = form.get("body"); 33 | // 兼容旧版的写法 34 | if (body == null) { 35 | body = form.get("controls"); 36 | } 37 | ArrayList ret = new ArrayList(); 38 | 39 | if (body == null) { 40 | return ret; 41 | } 42 | 43 | // 后面需要的数据类型 44 | SimpleBindings dataBindings = new SimpleBindings(); 45 | if (data.isObject()) { 46 | // 用于内嵌的 data 47 | SimpleBindings dataInnerBindings = new SimpleBindings(); 48 | Iterator> it = data.fields(); 49 | while (it.hasNext()) { 50 | Map.Entry entry = it.next(); 51 | dataBindings.put(entry.getKey(), entry.getValue()); 52 | dataInnerBindings.put(entry.getKey(), entry.getValue()); 53 | } 54 | dataBindings.put("data", dataInnerBindings); 55 | 56 | } 57 | 58 | // 表单级别校验 59 | JsonNode rules = form.get("rules"); 60 | if (rules != null && rules.isArray()) { 61 | for (JsonNode ruleProps : rules) { 62 | JsonNode rule = ruleProps.get("rule"); 63 | JsonNode message = ruleProps.get("message"); 64 | if (rule != null && message != null) { 65 | boolean ruleValue = Script.eval(rule.asText(), dataBindings); 66 | if (!ruleValue) { 67 | ret.add(new ConstraintViolation("", message.asText())); 68 | } 69 | } 70 | } 71 | } 72 | 73 | // 只有一个表单项的情况 74 | if (body.isObject()) { 75 | return validateFormItem(body, data, dataBindings); 76 | } else if (body.isArray()) { 77 | for (JsonNode formItem : body) { 78 | ret.addAll(validateFormItem(formItem, data, dataBindings)); 79 | } 80 | return ret; 81 | } 82 | 83 | return ret; 84 | } 85 | 86 | // 就是多了自动解析 JSON 87 | public static List validate(String amisSchemaStr, String data) 88 | throws JsonProcessingException { 89 | return validate(JSONHelper.toJSONNode(amisSchemaStr), JSONHelper.toJSONNode(data)); 90 | } 91 | 92 | /** 93 | * 根据 amis schema 和表单名进行自动校验 94 | * 和前面比就是多了自动转成 JSON 节点和查找对应的表单 schema 95 | * 96 | * @param amisSchemaStr amis schema 的字符串 97 | * @param formName 表单名 98 | * @param data 需要校验的数据 99 | * @return 违反规则的列表,如果列表为空意味着没有违反 100 | * @throws JsonProcessingException 101 | */ 102 | public static List validate(String amisSchemaStr, String formName, String data) 103 | throws JsonProcessingException { 104 | return validate(amisSchemaStr, formName, JSONHelper.toJSONNode(data)); 105 | } 106 | 107 | public static List validate(String amisSchemaStr, String formName, JsonNode data) 108 | throws JsonProcessingException { 109 | JsonNode amisSchema = JSONHelper.findObject(JSONHelper.toJSONNode(amisSchemaStr), 110 | (String key, JsonNode node, JsonNode parentNode) -> { 111 | if (key != null && key.equals("name")) { 112 | String type = parentNode.get("type").asText(); 113 | if (Objects.equals(type, "form")) { 114 | return node.asText().equals(formName); 115 | } 116 | } 117 | return false; 118 | } 119 | ); 120 | return validate(amisSchema, data); 121 | } 122 | 123 | /** 124 | * 用于生成验证消息 125 | * @param name 字段名 126 | * @param formItemSchema 表单项的配置 127 | * @param violationName 违反规则名 128 | * @param defaultMessage 默认消息 129 | * @param extInfo 用于某些报错辅助,比如大于多少 130 | * @return 131 | */ 132 | private static ConstraintViolation buildConstraintViolation(String name, JsonNode formItemSchema, 133 | String violationName, 134 | String defaultMessage, String extInfo) { 135 | String message = defaultMessage; 136 | JsonNode validationErrors = formItemSchema.get("validationErrors"); 137 | if (validationErrors != null && validationErrors.isObject()) { 138 | JsonNode customValidationMessage = validationErrors.get(violationName); 139 | if (customValidationMessage != null && !customValidationMessage.asText().isEmpty()) { 140 | message = customValidationMessage.asText(); 141 | } 142 | } 143 | 144 | if (extInfo != null) { 145 | message = message.replace("$1", extInfo); 146 | } 147 | 148 | return new ConstraintViolation(name, message); 149 | } 150 | 151 | private static ConstraintViolation buildConstraintViolation(String name, JsonNode formItemSchema, 152 | String violationName, 153 | String defaultMessage) { 154 | 155 | return buildConstraintViolation(name, formItemSchema, violationName, defaultMessage, null); 156 | } 157 | 158 | 159 | /** 160 | * 验证单个表单项 161 | * 162 | * @param formItemSchema 表单项 163 | * @param data 数据 164 | * @return 如果数组非空就代表验证不通过 165 | */ 166 | public static List validateFormItem(JsonNode formItemSchema, JsonNode data, 167 | SimpleBindings dataBindings) { 168 | JsonNode name = formItemSchema.get("name"); 169 | 170 | if (name == null) { 171 | return new ArrayList(); 172 | } 173 | JsonNode itemData = data.get(name.asText()); 174 | JsonNode validations = formItemSchema.get("validations"); 175 | 176 | // 将 requireOn 转成 isRequired 177 | boolean hasRequireOn = false; 178 | JsonNode requireOn = formItemSchema.get("requireOn"); 179 | if (requireOn != null && !requireOn.asText().isEmpty()) { 180 | hasRequireOn = Script.eval(requireOn.asText(), dataBindings); 181 | } 182 | 183 | // required 转成 isRequired 校验 184 | boolean hasRequire = false; 185 | JsonNode required = formItemSchema.get("required"); 186 | if (required != null && required.asBoolean()) { 187 | hasRequire = true; 188 | } 189 | 190 | if (hasRequireOn || hasRequire) { 191 | // 如果是 null 就新建一个 192 | if (validations == null) { 193 | ObjectMapper mapper = new ObjectMapper(); 194 | ObjectNode node = mapper.createObjectNode(); 195 | node.put("isRequired", true); 196 | validations = node; 197 | } else { 198 | ((ObjectNode) validations).put("isRequired", true); 199 | } 200 | } 201 | 202 | 203 | if (validations == null) { 204 | return new ArrayList(); 205 | } 206 | 207 | // visibleOn 和 hiddenOn 208 | JsonNode visibleOn = formItemSchema.get("visibleOn"); 209 | if (visibleOn != null && !visibleOn.asText().isEmpty()) { 210 | boolean res = Script.eval(visibleOn.asText(), dataBindings); 211 | if (!res) { 212 | return new ArrayList(); 213 | } 214 | } 215 | JsonNode hiddenOn = formItemSchema.get("hiddenOn"); 216 | if (hiddenOn != null && !hiddenOn.asText().isEmpty()) { 217 | boolean res = Script.eval(hiddenOn.asText(), dataBindings); 218 | if (res) { 219 | return new ArrayList(); 220 | } 221 | } 222 | 223 | // "hidden": true 或者 "visible": false 的表单项相当于禁用了,不做处理 224 | JsonNode visible = formItemSchema.get("visible"); 225 | if (visible != null && !visible.asBoolean()) { 226 | return new ArrayList(); 227 | } 228 | JsonNode hidden = formItemSchema.get("hidden"); 229 | if (hidden != null && hidden.asBoolean()) { 230 | return new ArrayList(); 231 | } 232 | 233 | JsonNodeType validationsNodeType = validations.getNodeType(); 234 | // 将老 string 写法转成新的对象方式 235 | if (validationsNodeType == JsonNodeType.STRING) { 236 | ObjectMapper mapper = new ObjectMapper(); 237 | ObjectNode validationObject = mapper.createObjectNode(); 238 | String[] validateWithValues = validations.asText().split(","); 239 | for (String validateWithValue : validateWithValues) { 240 | String[] validateAndValue = validateWithValue.split(":"); 241 | if (validateAndValue.length > 1) { 242 | validationObject.put(validateAndValue[0], validateAndValue[1]); 243 | } else { 244 | validationObject.put(validateAndValue[0], true); 245 | } 246 | } 247 | validations = validationObject; 248 | } 249 | 250 | 251 | 252 | if (validations != null && validations.isObject()) { 253 | 254 | 255 | ArrayList violationResult = new ArrayList(); 256 | Iterator> it = validations.fields(); 257 | while (it.hasNext()) { 258 | Map.Entry entry = it.next(); 259 | String validateName = entry.getKey(); 260 | JsonNode validateOption = entry.getValue(); 261 | // 用反射会导致不好查找代码,挨个写好了 262 | switch (validateName) { 263 | case "isRequired": 264 | if (!ValidationFn.isRequired(itemData)) { 265 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isRequired", 266 | ViolationMessage.isRequired)); 267 | } 268 | break; 269 | case "isExisty": 270 | if (!ValidationFn.isExisty(itemData)) { 271 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isExisty", 272 | ViolationMessage.isExisty)); 273 | } 274 | break; 275 | case "isEmail": 276 | if (!ValidationFn.isEmail(itemData)) { 277 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isEmail", 278 | ViolationMessage.isEmail)); 279 | } 280 | break; 281 | case "isUrl": 282 | if (!ValidationFn.isUrl(itemData)) { 283 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isUrl", 284 | ViolationMessage.isUrl)); 285 | } 286 | break; 287 | case "isInt": 288 | if (!ValidationFn.isInt(itemData)) { 289 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isInt", 290 | ViolationMessage.isInt)); 291 | } 292 | break; 293 | case "isAlpha": 294 | if (!ValidationFn.isAlpha(itemData)) { 295 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isAlpha", 296 | ViolationMessage.isAlpha)); 297 | } 298 | break; 299 | case "isNumeric": 300 | if (!ValidationFn.isNumeric(itemData)) { 301 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isNumeric", 302 | ViolationMessage.isNumeric)); 303 | } 304 | break; 305 | case "isAlphanumeric": 306 | if (!ValidationFn.isAlphanumeric(itemData)) { 307 | violationResult.add( 308 | buildConstraintViolation(name.asText(), formItemSchema, "isAlphanumeric", 309 | ViolationMessage.isAlphanumeric)); 310 | } 311 | break; 312 | case "isFloat": 313 | if (!ValidationFn.isFloat(itemData)) { 314 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isFloat", 315 | ViolationMessage.isFloat)); 316 | } 317 | break; 318 | case "isWords": 319 | if (!ValidationFn.isWords(itemData)) { 320 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isWords", 321 | ViolationMessage.isWords)); 322 | } 323 | break; 324 | case "isUrlPath": 325 | if (!ValidationFn.isUrlPath(itemData)) { 326 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isUrlPath", 327 | ViolationMessage.isUrlPath)); 328 | } 329 | break; 330 | case "matchRegexp": 331 | if (!ValidationFn.matchRegexp(itemData, validateOption.asText())) { 332 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "matchRegexp", 333 | ViolationMessage.matchRegexp, validateOption.asText())); 334 | } 335 | break; 336 | case "minLength": 337 | if (!ValidationFn.minLength(itemData, validateOption.asInt())) { 338 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "minLength", 339 | ViolationMessage.minLength, validateOption.asText())); 340 | } 341 | break; 342 | case "maxLength": 343 | if (!ValidationFn.maxLength(itemData, validateOption.asInt())) { 344 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "maxLength", 345 | ViolationMessage.maxLength, validateOption.asText())); 346 | } 347 | break; 348 | case "maximum": 349 | if (!ValidationFn.maximum(itemData, validateOption.asDouble())) { 350 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "maximum", 351 | ViolationMessage.maximum, validateOption.asText())); 352 | } 353 | break; 354 | case "lt": 355 | if (!ValidationFn.lt(itemData, validateOption.asDouble())) { 356 | violationResult.add( 357 | buildConstraintViolation(name.asText(), formItemSchema, "lt", ViolationMessage.lt 358 | , validateOption.asText())); 359 | } 360 | break; 361 | case "minimum": 362 | if (!ValidationFn.minimum(itemData, validateOption.asDouble())) { 363 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "minimum", 364 | ViolationMessage.minimum, validateOption.asText())); 365 | } 366 | break; 367 | case "gt": 368 | if (!ValidationFn.gt(itemData, validateOption.asDouble())) { 369 | violationResult.add( 370 | buildConstraintViolation(name.asText(), formItemSchema, "gt", ViolationMessage.gt 371 | , validateOption.asText())); 372 | } 373 | break; 374 | case "isJson": 375 | if (!ValidationFn.isJson(itemData)) { 376 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isJson", 377 | ViolationMessage.isJson)); 378 | } 379 | break; 380 | case "isLength": 381 | if (!ValidationFn.isLength(itemData, validateOption.asInt())) { 382 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isLength", 383 | ViolationMessage.isLength, validateOption.asText())); 384 | } 385 | break; 386 | case "notEmptyString": 387 | if (!ValidationFn.notEmptyString(itemData)) { 388 | violationResult.add( 389 | buildConstraintViolation(name.asText(), formItemSchema, "notEmptyString", 390 | ViolationMessage.notEmptyString)); 391 | } 392 | break; 393 | case "equalsField": 394 | if (!ValidationFn.equalsField(itemData, validateOption.asText())) { 395 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "equalsField", 396 | ViolationMessage.equalsField, validateOption.asText())); 397 | } 398 | break; 399 | case "equals": 400 | if (!ValidationFn.equals(itemData, validateOption)) { 401 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isEmail", 402 | ViolationMessage.equals, validateOption.asText())); 403 | } 404 | break; 405 | case "isPhoneNumber": 406 | if (!ValidationFn.isPhoneNumber(itemData)) { 407 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isPhoneNumber", 408 | ViolationMessage.isPhoneNumber)); 409 | } 410 | break; 411 | case "isTelNumber": 412 | if (!ValidationFn.isTelNumber(itemData)) { 413 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isTelNumber", 414 | ViolationMessage.isTelNumber)); 415 | } 416 | break; 417 | case "isZipcode": 418 | if (!ValidationFn.isZipcode(itemData)) { 419 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isZipcode", 420 | ViolationMessage.isZipcode)); 421 | } 422 | break; 423 | case "isId": 424 | if (!ValidationFn.isId(itemData)) { 425 | violationResult.add(buildConstraintViolation(name.asText(), formItemSchema, "isId", 426 | ViolationMessage.isId)); 427 | } 428 | break; 429 | } 430 | } 431 | return violationResult; 432 | } 433 | return new ArrayList(); 434 | } 435 | 436 | } 437 | -------------------------------------------------------------------------------- /src/test/kotlin/com/baidu/amis/validation/ValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.baidu.amis.validation 2 | 3 | import org.junit.jupiter.api.Test 4 | import kotlin.test.assertEquals 5 | 6 | internal class ValidatorTest { 7 | @Test 8 | fun testSingleValidator() { 9 | val result1 = Validator.validate( 10 | """ 11 | { 12 | "type": "page", 13 | "body": { 14 | "type": "form", 15 | "name": "myForm", 16 | "api": "/api/mock2/form/saveForm", 17 | "body": [ 18 | { 19 | "type": "input-text", 20 | "label": "文本", 21 | "name": "text", 22 | "validations": { 23 | "isNumeric": true 24 | }, 25 | "description": "请输入数字类型文本" 26 | } 27 | ] 28 | } 29 | } 30 | """, "myForm", """ 31 | { 32 | "text": "a" 33 | } 34 | """.trimIndent() 35 | ) 36 | 37 | assertEquals(result1[0].message, ViolationMessage.isNumeric) 38 | 39 | // 1.1.x 版本的格式 40 | val result2 = Validator.validate( 41 | """ 42 | { 43 | "type": "page", 44 | "body": { 45 | "type": "form", 46 | "name": "myForm", 47 | "api": "/api/mock2/form/saveForm", 48 | "controls": [ 49 | { 50 | "type": "text", 51 | "label": "文本", 52 | "name": "text", 53 | "validations": { 54 | "isNumeric": true 55 | }, 56 | "description": "请输入数字类型文本" 57 | } 58 | ] 59 | } 60 | } 61 | """, "myForm", """ 62 | { 63 | "text": "a" 64 | } 65 | """.trimIndent() 66 | ) 67 | assertEquals(result2[0].message, ViolationMessage.isNumeric) 68 | 69 | // 都没有的话就不检查 70 | val result3 = Validator.validate( 71 | """ 72 | { 73 | "type": "page", 74 | "body": { 75 | "type": "form", 76 | "name": "myForm", 77 | "api": "/api/mock2/form/saveForm" 78 | } 79 | } 80 | """, "myForm", """ 81 | { 82 | "text": "a" 83 | } 84 | """.trimIndent() 85 | ) 86 | assertEquals(result3.size, 0) 87 | 88 | // 只有一个表单项的情况 89 | val result4 = Validator.validate( 90 | """ 91 | { 92 | "type": "page", 93 | "body": { 94 | "type": "form", 95 | "name": "myForm", 96 | "api": "/api/mock2/form/saveForm", 97 | "body": { 98 | "type": "input-text", 99 | "label": "文本", 100 | "name": "text", 101 | "validations": { 102 | "isNumeric": true 103 | }, 104 | "description": "请输入数字类型文本" 105 | } 106 | } 107 | } 108 | """, "myForm", """ 109 | { 110 | "text": "a" 111 | } 112 | """.trimIndent() 113 | ) 114 | 115 | assertEquals(result4[0].message, ViolationMessage.isNumeric) 116 | } 117 | 118 | @Test 119 | fun testRequire() { 120 | val result1 = Validator.validate( 121 | """ 122 | { 123 | "type": "page", 124 | "body": { 125 | "type": "form", 126 | "name": "myForm", 127 | "api": "/api/mock2/form/saveForm", 128 | "body": [ 129 | { 130 | "type": "input-text", 131 | "label": "文本", 132 | "name": "text", 133 | "required": true, 134 | "description": "请输入数字类型文本" 135 | } 136 | ] 137 | } 138 | } 139 | """, "myForm", """ 140 | { 141 | "b": "a" 142 | } 143 | """.trimIndent() 144 | ) 145 | assertEquals(result1[0].message, ViolationMessage.isRequired) 146 | 147 | val result2 = Validator.validate( 148 | """ 149 | { 150 | "type": "page", 151 | "body": { 152 | "type": "form", 153 | "name": "myForm", 154 | "api": "/api/mock2/form/saveForm", 155 | "body": [ 156 | { 157 | "type": "input-text", 158 | "label": "文本", 159 | "name": "text", 160 | "required": true, 161 | "validations": { 162 | "isNumeric": true 163 | }, 164 | "description": "请输入数字类型文本" 165 | } 166 | ] 167 | } 168 | } 169 | """, "myForm", """ 170 | { 171 | "b": "a" 172 | } 173 | """.trimIndent() 174 | ) 175 | 176 | assertEquals(result2.size, 2) 177 | } 178 | 179 | @Test 180 | fun testStringValidator() { 181 | val result1 = Validator.validate( 182 | """ 183 | { 184 | "type": "page", 185 | "body": { 186 | "type": "form", 187 | "name": "myForm", 188 | "api": "/api/mock2/form/saveForm", 189 | "body": [ 190 | { 191 | "type": "input-text", 192 | "label": "文本", 193 | "name": "text", 194 | "validations": "isNumeric,maximum:10", 195 | "description": "请输入数字类型文本" 196 | } 197 | ] 198 | } 199 | } 200 | """, "myForm", """ 201 | { 202 | "text": "a" 203 | } 204 | """.trimIndent() 205 | ) 206 | 207 | assertEquals(result1[0].message, ViolationMessage.isNumeric) 208 | 209 | val result2 = Validator.validate( 210 | """ 211 | { 212 | "type": "page", 213 | "body": { 214 | "type": "form", 215 | "name": "myForm", 216 | "api": "/api/mock2/form/saveForm", 217 | "body": [ 218 | { 219 | "type": "input-text", 220 | "label": "文本", 221 | "name": "text", 222 | "validations": "isNumeric,maximum:10", 223 | "description": "请输入数字类型文本" 224 | } 225 | ] 226 | } 227 | } 228 | """, "myForm", """ 229 | { 230 | "text": 12 231 | } 232 | """.trimIndent() 233 | ) 234 | assertEquals(result2[0].message, "当前输入值超出最大值 10") 235 | } 236 | 237 | @Test 238 | fun testHidden() { 239 | val result1 = Validator.validate( 240 | """ 241 | { 242 | "type": "page", 243 | "body": { 244 | "type": "form", 245 | "name": "myForm", 246 | "api": "/api/mock2/form/saveForm", 247 | "body": [ 248 | { 249 | "type": "input-text", 250 | "label": "文本", 251 | "name": "text", 252 | "hidden": true, 253 | "validations": "isNumeric", 254 | "description": "请输入数字类型文本" 255 | } 256 | ] 257 | } 258 | } 259 | """, "myForm", """ 260 | { 261 | "text": "a" 262 | } 263 | """.trimIndent() 264 | ) 265 | assertEquals(result1.size, 0) 266 | 267 | val result2 = Validator.validate( 268 | """ 269 | { 270 | "type": "page", 271 | "body": { 272 | "type": "form", 273 | "name": "myForm", 274 | "api": "/api/mock2/form/saveForm", 275 | "body": [ 276 | { 277 | "type": "input-text", 278 | "label": "文本", 279 | "name": "text", 280 | "hidden": false, 281 | "validations": "isNumeric", 282 | "description": "请输入数字类型文本" 283 | } 284 | ] 285 | } 286 | } 287 | """, "myForm", """ 288 | { 289 | "text": "a" 290 | } 291 | """.trimIndent() 292 | ) 293 | assertEquals(result2.size, 1) 294 | 295 | val result3 = Validator.validate( 296 | """ 297 | { 298 | "type": "page", 299 | "body": { 300 | "type": "form", 301 | "name": "myForm", 302 | "api": "/api/mock2/form/saveForm", 303 | "body": [ 304 | { 305 | "type": "input-text", 306 | "label": "文本", 307 | "name": "text", 308 | "visible": false, 309 | "validations": "isNumeric", 310 | "description": "请输入数字类型文本" 311 | } 312 | ] 313 | } 314 | } 315 | """, "myForm", """ 316 | { 317 | "text": "a" 318 | } 319 | """.trimIndent() 320 | ) 321 | 322 | assertEquals(result3.size, 0) 323 | 324 | val result4 = Validator.validate( 325 | """ 326 | { 327 | "type": "page", 328 | "body": { 329 | "type": "form", 330 | "name": "myForm", 331 | "api": "/api/mock2/form/saveForm", 332 | "body": [ 333 | { 334 | "type": "input-text", 335 | "label": "文本", 336 | "name": "text", 337 | "visible": true, 338 | "validations": "isNumeric", 339 | "description": "请输入数字类型文本" 340 | } 341 | ] 342 | } 343 | } 344 | """, "myForm", """ 345 | { 346 | "text": "a" 347 | } 348 | """.trimIndent() 349 | ) 350 | 351 | assertEquals(result4.size, 1) 352 | } 353 | 354 | @Test 355 | fun testCustomMessage() { 356 | val result1 = Validator.validate( 357 | """ 358 | { 359 | "type": "page", 360 | "body": { 361 | "type": "form", 362 | "name": "myForm", 363 | "api": "/api/mock2/form/saveForm", 364 | "body": [ 365 | { 366 | "type": "input-text", 367 | "label": "文本", 368 | "name": "text", 369 | "validations": { 370 | "isNumeric": true 371 | }, 372 | "validationErrors": { 373 | "isNumeric": "同学,请输入数字哈" 374 | }, 375 | "description": "请输入数字类型文本" 376 | } 377 | ] 378 | } 379 | } 380 | """, "myForm", """ 381 | { 382 | "text": "a" 383 | } 384 | """.trimIndent() 385 | ) 386 | 387 | assertEquals(result1[0].message, "同学,请输入数字哈") 388 | } 389 | 390 | @Test 391 | fun testRequireOn() { 392 | val result1 = Validator.validate( 393 | """ 394 | { 395 | "type": "page", 396 | "body": { 397 | "type": "form", 398 | "name": "myForm", 399 | "api": "/api/mock2/form/saveForm", 400 | "body": [ 401 | { 402 | "type": "input-text", 403 | "label": "文本", 404 | "name": "text", 405 | "requireOn": "this.a == 1" 406 | } 407 | ] 408 | } 409 | } 410 | """, "myForm", """ 411 | { 412 | "a": 1 413 | } 414 | """.trimIndent() 415 | ) 416 | 417 | assertEquals(result1[0].message, "这是必填项") 418 | } 419 | 420 | @Test 421 | fun testHiddenOn() { 422 | val formSchema = """ 423 | { 424 | "type": "page", 425 | "body": { 426 | "type": "form", 427 | "name": "myForm", 428 | "api": "/api/mock2/form/saveForm", 429 | "body": [ 430 | { 431 | "type": "input-text", 432 | "label": "文本", 433 | "name": "text", 434 | "validations": { 435 | "isNumeric": true 436 | }, 437 | "hiddenOn": "this.a == 1" 438 | } 439 | ] 440 | } 441 | } 442 | """; 443 | val result1 = Validator.validate( 444 | formSchema, "myForm", """ 445 | { 446 | "a": 1 447 | } 448 | """.trimIndent() 449 | ) 450 | 451 | assertEquals(result1.size, 0) 452 | 453 | val result2 = Validator.validate( 454 | formSchema, "myForm", """ 455 | """.trimIndent() 456 | ) 457 | 458 | assertEquals(result2[0].message, "请输入数字") 459 | } 460 | 461 | 462 | 463 | @Test 464 | fun testVisibleOn() { 465 | val formSchema = """ 466 | { 467 | "type": "page", 468 | "body": { 469 | "type": "form", 470 | "name": "myForm", 471 | "api": "/api/mock2/form/saveForm", 472 | "body": [ 473 | { 474 | "type": "input-text", 475 | "label": "文本", 476 | "name": "text", 477 | "validations": { 478 | "isNumeric": true 479 | }, 480 | "visibleOn": "this.a == 1" 481 | } 482 | ] 483 | } 484 | } 485 | """; 486 | val result1 = Validator.validate( 487 | formSchema, "myForm", """ 488 | { 489 | "a": 2 490 | } 491 | """.trimIndent() 492 | ) 493 | 494 | assertEquals(result1.size, 0) 495 | 496 | val result2 = Validator.validate( 497 | formSchema, "myForm", """ 498 | { 499 | "a": 1 500 | } 501 | """.trimIndent() 502 | ) 503 | 504 | assertEquals(result2[0].message, "请输入数字") 505 | } 506 | 507 | @Test 508 | fun testFormRules() { 509 | val formSchema = """ 510 | { 511 | "type": "page", 512 | "body": { 513 | "type": "form", 514 | "name": "myForm", 515 | "api": "/api/form/saveForm", 516 | "rules": [ 517 | { 518 | "rule": "!(data.a && data.b)", 519 | "message": "a 和 b 不能同时有值" 520 | } 521 | ], 522 | "body": [ 523 | { 524 | "type": "input-text", 525 | "name": "a", 526 | "label": "A" 527 | }, 528 | { 529 | "type": "input-text", 530 | "name": "b", 531 | "label": "B" 532 | } 533 | ] 534 | } 535 | } 536 | """.trimIndent() 537 | val result1 = Validator.validate( 538 | formSchema, "myForm", """ 539 | { 540 | "a": "a", 541 | "b": "b" 542 | } 543 | """.trimIndent() 544 | ) 545 | 546 | assertEquals(result1[0].message, "a 和 b 不能同时有值") 547 | 548 | val result2 = Validator.validate( 549 | formSchema, "myForm", """ 550 | { 551 | "a": "a" 552 | } 553 | """.trimIndent() 554 | ) 555 | 556 | assertEquals(result2.size, 0) 557 | } 558 | 559 | @Test 560 | fun testformItems() { 561 | val isNumeric = Validator.validate( 562 | """ 563 | { 564 | "type": "form", 565 | "body": { 566 | "type": "input-text", 567 | "name": "a", 568 | "label": "A", 569 | "validations": { 570 | "isNumeric": true 571 | } 572 | } 573 | } 574 | """.trimIndent(), """ 575 | { 576 | "a": "a" 577 | } 578 | """.trimIndent() 579 | ) 580 | assertEquals(isNumeric[0].message, "请输入数字") 581 | 582 | val isExisty = Validator.validate( 583 | """ 584 | { 585 | "type": "form", 586 | "body": { 587 | "type": "input-text", 588 | "name": "a", 589 | "label": "A", 590 | "validations": { 591 | "isExisty": true 592 | } 593 | } 594 | } 595 | """.trimIndent(), """ 596 | { 597 | "b": "a" 598 | } 599 | """.trimIndent() 600 | ) 601 | assertEquals(isExisty[0].message, "不存在这个值") 602 | 603 | val isEmail = Validator.validate( 604 | """ 605 | { 606 | "type": "form", 607 | "body": { 608 | "type": "input-text", 609 | "name": "a", 610 | "label": "A", 611 | "validations": { 612 | "isEmail": true 613 | } 614 | } 615 | } 616 | """.trimIndent(), """ 617 | { 618 | "a": "a" 619 | } 620 | """.trimIndent() 621 | ) 622 | assertEquals(isEmail[0].message, "Email 格式不正确") 623 | 624 | val isUrl = Validator.validate( 625 | """ 626 | { 627 | "type": "form", 628 | "body": { 629 | "type": "input-text", 630 | "name": "a", 631 | "label": "A", 632 | "validations": { 633 | "isUrl": true 634 | } 635 | } 636 | } 637 | """.trimIndent(), """ 638 | { 639 | "a": "a" 640 | } 641 | """.trimIndent() 642 | ) 643 | assertEquals(isUrl[0].message, "URL 格式不正确") 644 | 645 | val isAlpha = Validator.validate( 646 | """ 647 | { 648 | "type": "form", 649 | "body": { 650 | "type": "input-text", 651 | "name": "a", 652 | "label": "A", 653 | "validations": { 654 | "isAlpha": true 655 | } 656 | } 657 | } 658 | """.trimIndent(), """ 659 | { 660 | "a": 1 661 | } 662 | """.trimIndent() 663 | ) 664 | assertEquals(isAlpha[0].message, "请输入字母") 665 | 666 | val isAlphanumeric = Validator.validate( 667 | """ 668 | { 669 | "type": "form", 670 | "body": { 671 | "type": "input-text", 672 | "name": "a", 673 | "label": "A", 674 | "validations": { 675 | "isAlphanumeric": true 676 | } 677 | } 678 | } 679 | """.trimIndent(), """ 680 | { 681 | "a": "#$" 682 | } 683 | """.trimIndent() 684 | ) 685 | assertEquals(isAlphanumeric[0].message, "请输入字母或者数字") 686 | 687 | val isInt = Validator.validate( 688 | """ 689 | { 690 | "type": "form", 691 | "body": { 692 | "type": "input-text", 693 | "name": "a", 694 | "label": "A", 695 | "validations": { 696 | "isInt": true 697 | } 698 | } 699 | } 700 | """.trimIndent(), """ 701 | { 702 | "a": 1.2 703 | } 704 | """.trimIndent() 705 | ) 706 | assertEquals(isInt[0].message, "请输入整型数字") 707 | 708 | val isFloat = Validator.validate( 709 | """ 710 | { 711 | "type": "form", 712 | "body": { 713 | "type": "input-text", 714 | "name": "a", 715 | "label": "A", 716 | "validations": { 717 | "isFloat": true 718 | } 719 | } 720 | } 721 | """.trimIndent(), """ 722 | { 723 | "a": "ff" 724 | } 725 | """.trimIndent() 726 | ) 727 | assertEquals(isFloat[0].message, "请输入浮点型数值") 728 | 729 | val isWords = Validator.validate( 730 | """ 731 | { 732 | "type": "form", 733 | "body": { 734 | "type": "input-text", 735 | "name": "a", 736 | "label": "A", 737 | "validations": { 738 | "isWords": true 739 | } 740 | } 741 | } 742 | """.trimIndent(), """ 743 | { 744 | "a": 1 745 | } 746 | """.trimIndent() 747 | ) 748 | assertEquals(isWords[0].message, "请输入单词") 749 | 750 | val isUrlPath = Validator.validate( 751 | """ 752 | { 753 | "type": "form", 754 | "body": { 755 | "type": "input-text", 756 | "name": "a", 757 | "label": "A", 758 | "validations": { 759 | "isUrlPath": true 760 | } 761 | } 762 | } 763 | """.trimIndent(), """ 764 | { 765 | "a": " d" 766 | } 767 | """.trimIndent() 768 | ) 769 | assertEquals(isUrlPath[0].message, "只能输入字母、数字、`-` 和 `_`.") 770 | 771 | val matchRegexp = Validator.validate( 772 | """ 773 | { 774 | "type": "form", 775 | "body": { 776 | "type": "input-text", 777 | "name": "a", 778 | "label": "A", 779 | "validations": { 780 | "matchRegexp": "a" 781 | } 782 | } 783 | } 784 | """.trimIndent(), """ 785 | { 786 | "a": 1 787 | } 788 | """.trimIndent() 789 | ) 790 | assertEquals(matchRegexp[0].message, "格式不正确, 请输入符合规则为 a 的内容") 791 | 792 | val minLength = Validator.validate( 793 | """ 794 | { 795 | "type": "form", 796 | "body": { 797 | "type": "input-text", 798 | "name": "a", 799 | "label": "A", 800 | "validations": { 801 | "minLength": 3 802 | } 803 | } 804 | } 805 | """.trimIndent(), """ 806 | { 807 | "a": "dd" 808 | } 809 | """.trimIndent() 810 | ) 811 | assertEquals(minLength[0].message, "请输入更多的内容,至少输入 3 个字符") 812 | 813 | val maxLength = Validator.validate( 814 | """ 815 | { 816 | "type": "form", 817 | "body": { 818 | "type": "input-text", 819 | "name": "a", 820 | "label": "A", 821 | "validations": { 822 | "maxLength": 1 823 | } 824 | } 825 | } 826 | """.trimIndent(), """ 827 | { 828 | "a": "dd" 829 | } 830 | """.trimIndent() 831 | ) 832 | assertEquals(maxLength[0].message, "请控制内容长度, 不要输入 1 个以上字符") 833 | 834 | val maximum = Validator.validate( 835 | """ 836 | { 837 | "type": "form", 838 | "body": { 839 | "type": "input-text", 840 | "name": "a", 841 | "label": "A", 842 | "validations": { 843 | "maximum": 20 844 | } 845 | } 846 | } 847 | """.trimIndent(), """ 848 | { 849 | "a": 21 850 | } 851 | """.trimIndent() 852 | ) 853 | 854 | assertEquals(maximum[0].message, "当前输入值超出最大值 20") 855 | 856 | val lt = Validator.validate( 857 | """ 858 | { 859 | "type": "form", 860 | "body": { 861 | "type": "input-text", 862 | "name": "a", 863 | "label": "A", 864 | "validations": { 865 | "lt": 20 866 | } 867 | } 868 | } 869 | """.trimIndent(), """ 870 | { 871 | "a": 21 872 | } 873 | """.trimIndent() 874 | ) 875 | 876 | assertEquals(lt[0].message, "请输入小于 20 的值") 877 | 878 | val minimum = Validator.validate( 879 | """ 880 | { 881 | "type": "form", 882 | "body": { 883 | "type": "input-text", 884 | "name": "a", 885 | "label": "A", 886 | "validations": { 887 | "minimum": 10 888 | } 889 | } 890 | } 891 | """.trimIndent(), """ 892 | { 893 | "a": 9 894 | } 895 | """.trimIndent() 896 | ) 897 | assertEquals(minimum[0].message, "当前输入值低于最小值 10") 898 | 899 | val gt = Validator.validate( 900 | """ 901 | { 902 | "type": "form", 903 | "body": { 904 | "type": "input-text", 905 | "name": "a", 906 | "label": "A", 907 | "validations": { 908 | "gt": 10 909 | } 910 | } 911 | } 912 | """.trimIndent(), """ 913 | { 914 | "a": 1 915 | } 916 | """.trimIndent() 917 | ) 918 | assertEquals(gt[0].message, "请输入大于 10 的值") 919 | 920 | val isJson = Validator.validate( 921 | """ 922 | { 923 | "type": "form", 924 | "body": { 925 | "type": "input-text", 926 | "name": "a", 927 | "label": "A", 928 | "validations": { 929 | "isJson": true 930 | } 931 | } 932 | } 933 | """.trimIndent(), """ 934 | { 935 | "a": "a" 936 | } 937 | """.trimIndent() 938 | ) 939 | assertEquals(isJson[0].message, "JSON 格式不正确") 940 | 941 | val isLength = Validator.validate( 942 | """ 943 | { 944 | "type": "form", 945 | "body": { 946 | "type": "input-text", 947 | "name": "a", 948 | "label": "A", 949 | "validations": { 950 | "isLength": 2 951 | } 952 | } 953 | } 954 | """.trimIndent(), """ 955 | { 956 | "a": "b" 957 | } 958 | """.trimIndent() 959 | ) 960 | assertEquals(isLength[0].message, "请输入长度为 2 的内容") 961 | 962 | val notEmptyString = Validator.validate( 963 | """ 964 | { 965 | "type": "form", 966 | "body": { 967 | "type": "input-text", 968 | "name": "a", 969 | "label": "A", 970 | "validations": { 971 | "notEmptyString": true 972 | } 973 | } 974 | } 975 | """.trimIndent(), """ 976 | { 977 | "a": "" 978 | } 979 | """.trimIndent() 980 | ) 981 | assertEquals(notEmptyString[0].message, "请不要全输入空白字符") 982 | 983 | val equalsField = Validator.validate( 984 | """ 985 | { 986 | "type": "form", 987 | "body": { 988 | "type": "input-text", 989 | "name": "a", 990 | "label": "A", 991 | "validations": { 992 | "equalsField": "b" 993 | } 994 | } 995 | } 996 | """.trimIndent(), """ 997 | { 998 | "a": 1, 999 | "b": 2 1000 | } 1001 | """.trimIndent() 1002 | ) 1003 | assertEquals(equalsField[0].message, "输入的数据与 b 值不一致") 1004 | 1005 | val equals = Validator.validate( 1006 | """ 1007 | { 1008 | "type": "form", 1009 | "body": { 1010 | "type": "input-text", 1011 | "name": "a", 1012 | "label": "A", 1013 | "validations": { 1014 | "equals": 2 1015 | } 1016 | } 1017 | } 1018 | """.trimIndent(), """ 1019 | { 1020 | "a": 1 1021 | } 1022 | """.trimIndent() 1023 | ) 1024 | assertEquals(equals[0].message, "输入的数据与 2 不一致") 1025 | 1026 | val isPhoneNumber = Validator.validate( 1027 | """ 1028 | { 1029 | "type": "form", 1030 | "body": { 1031 | "type": "input-text", 1032 | "name": "a", 1033 | "label": "A", 1034 | "validations": { 1035 | "isPhoneNumber": true 1036 | } 1037 | } 1038 | } 1039 | """.trimIndent(), """ 1040 | { 1041 | "a": 1 1042 | } 1043 | """.trimIndent() 1044 | ) 1045 | assertEquals(isPhoneNumber[0].message, "请输入合法的手机号码") 1046 | 1047 | 1048 | val isTelNumber = Validator.validate( 1049 | """ 1050 | { 1051 | "type": "form", 1052 | "body": { 1053 | "type": "input-text", 1054 | "name": "a", 1055 | "label": "A", 1056 | "validations": { 1057 | "isTelNumber": true 1058 | } 1059 | } 1060 | } 1061 | """.trimIndent(), """ 1062 | { 1063 | "a": 1 1064 | } 1065 | """.trimIndent() 1066 | ) 1067 | assertEquals(isTelNumber[0].message, "请输入合法的电话号码") 1068 | 1069 | val isZipcode = Validator.validate( 1070 | """ 1071 | { 1072 | "type": "form", 1073 | "body": { 1074 | "type": "input-text", 1075 | "name": "a", 1076 | "label": "A", 1077 | "validations": { 1078 | "isZipcode": true 1079 | } 1080 | } 1081 | } 1082 | """.trimIndent(), """ 1083 | { 1084 | "a": 1 1085 | } 1086 | """.trimIndent() 1087 | ) 1088 | assertEquals(isZipcode[0].message, "请输入合法的邮编地址") 1089 | 1090 | val isId = Validator.validate( 1091 | """ 1092 | { 1093 | "type": "form", 1094 | "body": { 1095 | "type": "input-text", 1096 | "name": "a", 1097 | "label": "A", 1098 | "validations": { 1099 | "isId": true 1100 | } 1101 | } 1102 | } 1103 | """.trimIndent(), """ 1104 | { 1105 | "a": 1 1106 | } 1107 | """.trimIndent() 1108 | ) 1109 | assertEquals(isId[0].message, "请输入合法的身份证号") 1110 | 1111 | 1112 | 1113 | } 1114 | } --------------------------------------------------------------------------------