[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report/报告 bug\ndescription: \"Create a report to help us improve, please read FAQ first./帮助我们更好地改进项目，但请先阅读常见问题与提问前必看，不要提已有的重复问题！\"\ntitle: \"[Bug] \"\nlabels: [kind/bug]\nbody:\n- type: markdown\n  attributes:\n    value: \"如果你已经知道问题所在、怎么解决，请直接 提交 Pull Request 为社区做贡献，非常感谢。\\n开发者也是人，也需要工作、休息、恋爱、陪伴家人、走亲会友等，也有心情不好和身体病痛，\\n往往没有额外的时间精力顾及一些小问题，请理解和支持，开源要大家参与贡献才会更美好~\\n少数个人的热情终有被耗尽的一天，只有大家共同建设和繁荣社区，才能让开源可持续发展！ \"\n\n- type: input\n  attributes:\n    label: APIJSON Version/APIJSON 版本号\n    placeholder: |\n      e.g./例如 5.4.0 ，如果不是最新版请用最新版，复现问题再来，原则上不更新旧版，而是只维护一个最新版\n\n  validations:\n    required: true\n\n- type: input\n  attributes:\n    label: Database Type & Version/数据库类型及版本号\n    placeholder: |\n      e.g./例如 MySQL 5.7.34\n        \n  validations:\n    required: true\n    \n- type: textarea\n  attributes:\n    label: Environment/环境信息\n    description: |\n      e.g./例如：\n         - **JDK/基础库**: 1.8.0_17\n         - **OS/系统**: MacOS Monterey 12.6 (21G115) M1\n    value: |\n        - JDK/基础库:\n        - OS/系统:\n    render: markdown\n\n  validations:\n    required: true\n    \n- type: input\n  attributes:\n    label: APIAuto Screenshots/APIAuto 请求与结果完整截屏\n    description: \"Upload by copy and paste image file or url./复制图片文件或 URL 再粘贴到输入框(用 APIAuto 能静态检查出很多问题，甚至还有修复建议，不用浪费你我的时间)\\n https://github.com/TommyLemon/APIAuto \"\n    value: \n\n  validations:\n    required: true\n    \n- type: textarea\n  attributes:\n    label: Current Behavior/问题描述\n    description: \"A concise description of what you're experiencing. Must contains screenshots./\\n\\n**提 bug 请发请求和响应的【完整截屏】，没图的自行解决！\\n开发者有限的时间和精力主要放在【维护项目源码和文档】上！\\n【描述不详细】 或 【文档/常见问题 已有答案】 的问题可能会被忽略！！\\n【态度 不文明/不友善】的可能会被拉黑，问题也可能不予解答！！！**\\n\\n请求参数 JSON 中表名、字段名、关键词及对应的值都是大小写敏感、逗号敏感、分号敏感、空格敏感、换行敏感，\\n大部分情况都不允许空格和换行，表名以大写字母开头，不要想当然，请严格按照 设计规范 来调用 API \\n https://github.com/Tencent/APIJSON/issues/181 \"\n    render: markdown\n\n  validations:\n    required: true\n\n- type: textarea\n  attributes:\n    label: Expected Behavior/期望结果\n    description: A concise description of what you expected to happen./具体描述你期望返回什么样的结果或者达到什么样的效果？\n    render: markdown\n\n  validations:\n    required: false\n\n\n- type: textarea\n  attributes:\n    label: Any additional comments?/其它补充说明？\n    description: |\n      e.g. some background/context of how you ran into this bug./例如：一些背景或上下文信息，包括复现步骤、相关日志等\n    render: markdown\n\n  validations:\n    required: false\n    \n- type: markdown\n  attributes:\n    value: \"Please follow the rules to fulfil all required inputs. You can add screenshots by comment after submit this issue./\\n请按要求填写所有必填项，未填完将提交不了！\\n如果随意填写敷衍了事，将直接关闭 issue，问题不会得到解答！\\n可以提交后再通过回复评论来补充上传截屏图片(复制粘贴文件)。\\n如果是网页 bug 等与你无关的原因导致提交不了，可以改为填问卷：\\n https://wj.qq.com/s2/10971431/2a09 \"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request/期望新增功能\ndescription: Request a new feature/期望新增什么样的功能或特性，或者做哪些方面的改进？\ntitle: \"[Feature] \"\nlabels: [kind/feature]\nbody:\n- type: textarea\n  attributes:\n    label: Description\n    description: |\n      Please describe what this feature does./具体描述下是什么样的功能或特性，以及你为什么想要它，用在什么场景，碰到了什么痛点，有什么解决思路，尝试过哪些，效果怎样？\n\n  validations:\n    required: true\n    \n- type: markdown\n  attributes:\n    value: 推荐去建议收集箱提问，也方便 统一检索和管理、投票决定优先级、更新处理进度 等：    https://github.com/Tencent/APIJSON/issues/37\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/other_issues.yml",
    "content": "name: Other Issues/其它反馈\ndescription: For questions, suggestions, improvements and others./问题(非 bug)、建议(非新增功能) 或 其它\ntitle: \"[xxx] \"\nbody:\n- type: textarea\n  attributes:\n    label: Description\n    description: |\n      Please describe the issue./请具体描述，包括是什么、为什么、如何做\n\n  validations:\n    required: true\n\n- type: markdown\n  attributes:\n    value: \"Bug 反馈请使用正确的模板，用错模板将直接关闭 issue，不予解答：\\n https://github.com/Tencent/APIJSON/issues/new?assignees=&labels=kind%2Fbug&template=bug_report.yml&title=%5BBug%5D+ \\n有建议请去建议收集箱提问，也方便 统一检索和管理、投票决定优先级、更新处理进度 等：\\n https://github.com/Tencent/APIJSON/issues/37 \"\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.gradle\nyarn.lock\n*.project\n\nHELP.md\ntarget/\n!.mvn/wrapper/maven-wrapper.jar\n!**/src/main/**\n!**/src/test/**\n\n### STS ###\n.apt_generated\n.classpath\n.factorypath\n.project\n.settings\n.springBeans\n.sts4-cache\n\n### IntelliJ IDEA ###\n.idea\n*.iws\n*.iml\n*.ipr\n\n### NetBeans ###\n/nbproject/private/\n/nbbuild/\n/dist/\n/nbdist/\n/.nb-gradle/\nbuild/\n\n### VS Code ###\n.vscode/\nAPIJSONORM/bin\n*.DS_Store\n"
  },
  {
    "path": "APIJSONORM/.gitignore",
    "content": "/target/\n"
  },
  {
    "path": "APIJSONORM/README.md",
    "content": "# APIJSONORM  [![](https://jitpack.io/v/Tencent/APIJSON.svg)](https://jitpack.io/#Tencent/APIJSON) [<img src=\"https://devin.ai/assets/deepwiki-badge.png\" alt=\"Ask DeepWiki.com\" height=\"20\"/>](https://deepwiki.com/Tencent/APIJSON)\n腾讯 [APIJSON](https://github.com/Tencent/APIJSON) ORM 库，可通过 Maven, Gradle 等远程依赖。<br />\nTencent [APIJSON](https://github.com/Tencent/APIJSON) ORM library for remote dependencies with Maven, Gradle, etc.\n\n### Maven\n#### 1. 在 pom.xml 中添加 JitPack 仓库\n#### 1. Add the JitPack repository to pom.xml\n```xml\n\t<repositories>\n\t\t<repository>\n\t\t    <id>jitpack.io</id>\n\t\t    <url>https://jitpack.io</url>\n\t\t</repository>\n\t</repositories>\n```\n<br />\n\n#### 2. 在 pom.xml 中添加 APIJSON 依赖\n#### 2. Add the APIJSON dependency to pom.xml\n```xml\n\t<dependency>\n\t    <groupId>com.github.Tencent</groupId>\n\t    <artifactId>APIJSON</artifactId>\n\t    <version>LATEST</version>\n\t</dependency>\n```\n\n<br />\n<br />\n\n### Gradle\n#### 1. 在项目根目录 build.gradle 中最后添加 JitPack 仓库\n#### 1. Add the JitPack repository in your root build.gradle at the end of repositories\n```gradle\n\tallprojects {\n\t\trepositories {\n\t\t\t...\n\t\t\tmaven { url 'https://jitpack.io' }\n\t\t}\n\t}\n```\n<br />\n\n#### 2. 在项目某个 module 目录(例如 `app`) build.gradle 中添加 apijson-orm 依赖\n#### 2. Add the APIJSON dependency in one of your modules(such as `app`)\n```gradle\n\tdependencies {\n\t        implementation 'com.github.Tencent:APIJSON:latest'\n\t}\n```\n\n<br />\n<br />\n\n### FASTJSON 2\n#### Code\nhttps://github.com/Tencent/APIJSON/tree/fastjson2\n\n#### Maven\nhttps://mvnrepository.com/artifact/com.github.linushp/zikai-apijson/1.0\n\n<br />\n\n### Unit Test\nhttp://apijson.cn/unit\n"
  },
  {
    "path": "APIJSONORM/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\n\t<groupId>com.github.Tencent</groupId>\n\t<artifactId>APIJSON</artifactId>\n\t<version>8.1.3</version>\n\t<packaging>jar</packaging>\n\n\t<name>APIJSONORM</name>\n\t<description>APIJSON ORM Library</description>\n\n\t<properties>\n\t\t<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n\t\t<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n\t\t<java.version>1.8</java.version>\n\t\t<maven.compiler.encoding>UTF-8</maven.compiler.encoding>\n\t\t<maven.compiler.source>1.8</maven.compiler.source>\n\t\t<maven.compiler.target>1.8</maven.compiler.target>\n\t</properties>\n\n\t<dependencies>\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-compiler-plugin</artifactId>\n\t\t\t\t<version>3.12.1</version>\n\t\t\t\t<configuration>\n\t\t\t\t\t<source>1.8</source>\n\t\t\t\t\t<target>1.8</target>\n\t\t\t\t</configuration>\n\t\t\t</plugin>\n\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.apache.maven.plugins</groupId>\n\t\t\t\t<artifactId>maven-source-plugin</artifactId>\n\t\t\t\t<version>3.2.1</version>\n\t\t\t\t<executions>\n\t\t\t\t\t<execution>\n\t\t\t\t\t\t<phase>package</phase>\n\t\t\t\t\t\t<goals>\n\t\t\t\t\t\t\t<goal>jar-no-fork</goal>\n\t\t\t\t\t\t</goals>\n\t\t\t\t\t</execution>\n\t\t\t\t</executions>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n\n</project>\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSON.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\npackage apijson;\n\nimport java.util.Collection;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**JSON工具类 防止解析时异常\n * @author Lemon\n */\npublic class JSON {\n\n\tstatic final String TAG = \"JSON\";\n\n\tpublic static JSONParser<? extends Map<String, Object>, ? extends List<Object>> DEFAULT_JSON_PARSER;\n\n\tstatic {\n\t\t//DEFAULT_JSON_PARSER = new JSONParser<LinkedHashMap<String, Object>, List<Object>>() {\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic LinkedHashMap<String, Object> createJSONObject() {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic List<Object> createJSONArray() {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic String toJSONString(Object obj, boolean format) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic Object parse(Object json) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic LinkedHashMap<String, Object> parseObject(Object json) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic <T> T parseObject(Object json, Class<T> clazz) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic List<Object> parseArray(Object json) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//\t@Override\n\t\t//\tpublic <T> List<T> parseArray(Object json, Class<T> clazz) {\n\t\t//\t\tthrow new UnsupportedOperationException();\n\t\t//\t}\n\t\t//\n\t\t//};\n\n\t}\n\n//\tpublic static JSONCreator<? extends Map<String, Object>, ? extends List<Object>> DEFAULT_JSON_CREATOR = DEFAULT_JSON_PARSER;\n//\tpublic static <M extends Map<String, Object>> M newObj() {\n//\t\treturn createJSONObject();\n//\t}\n//\tpublic static <M extends Map<String, Object>> M newObj(String key, Object value) {\n//\t\treturn createJSONObject(key, value);\n//\t}\n//\tpublic static <M extends Map<String, Object>> M newObj(Map<? extends String, ?> map) {\n//\t\treturn createJSONObject(map);\n//\t}\n\n\tpublic static <M extends Map<String, Object>> M createJSONObject() {\n\t\treturn (M) DEFAULT_JSON_PARSER.createJSONObject();\n\t}\n\tpublic static <M extends Map<String, Object>> M createJSONObject(String key, Object value) {\n\t\treturn (M) DEFAULT_JSON_PARSER.createJSONObject(key, value);\n\t}\n\tpublic static <M extends Map<String, Object>> M createJSONObject(Map<? extends String, ?> map) {\n\t\treturn (M) DEFAULT_JSON_PARSER.createJSONObject(map);\n\t}\n\n\t//public static <L extends List<Object>> L newArr() {\n\t//\treturn createJSONArray();\n\t//}\n\t//public static <L extends List<Object>> L newArr(Object obj) {\n\t//\treturn createJSONArray(obj);\n\t//}\n\t//public static <L extends List<Object>> L newArr(List<?> list) {\n\t//\treturn createJSONArray(list);\n\t//}\n\n\tpublic static <L extends List<Object>> L createJSONArray() {\n\t\treturn (L) DEFAULT_JSON_PARSER.createJSONArray();\n\t}\n\tpublic static <L extends List<Object>> L createJSONArray(Object obj) {\n\t\treturn (L) DEFAULT_JSON_PARSER.createJSONArray(obj);\n\t}\n\tpublic static <L extends List<Object>> L createJSONArray(Collection<?> list) {\n\t\treturn (L) DEFAULT_JSON_PARSER.createJSONArray(list);\n\t}\n\n\tpublic static Object parse(Object json) {\n\t\treturn DEFAULT_JSON_PARSER.parse(json);\n\t}\n\n\n\tpublic static <M extends Map<String, Object>> M parseObject(Object json) {\n\t\tString s = toJSONString(json);\n\t\tif (StringUtil.isEmpty(s, true)) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn (M) DEFAULT_JSON_PARSER.parseObject(s);\n\t}\n\n\tpublic static <T> T parseObject(Object json, Class<T> clazz) {\n\t\tString s = toJSONString(json);\n\t\tif (StringUtil.isEmpty(s, true)) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn DEFAULT_JSON_PARSER.parseObject(s, clazz);\n\t}\n\n\t/**\n\t * @param json\n\t * @return\n\t */\n\tpublic static <L extends List<Object>> L parseArray(Object json) {\n\t\tString s = toJSONString(json);\n\t\tif (StringUtil.isEmpty(s, true)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tL arr = (L) DEFAULT_JSON_PARSER.parseArray(s);\n\t\t\treturn arr;\n\t\t} catch (Exception e) {\n\t\t\tLog.i(TAG, \"parseArray catch \\n\" + e.getMessage());\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic static <T> List<T> parseArray(Object json, Class<T> clazz) {\n\t\tString s = toJSONString(json);\n\t\tif (StringUtil.isEmpty(s, true)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\treturn DEFAULT_JSON_PARSER.parseArray(s, clazz);\n\t\t} catch (Exception e) {\n\t\t\tLog.i(TAG, \"parseArray catch \\n\" + e.getMessage());\n\t\t}\n\t\treturn null;\n\t}\n\n\t/**\n\t * @param obj\n\t * @return\n\t */\n\tpublic static String format(Object obj) {\n\t\treturn toJSONString(obj, true);\n\t}\n\t/**\n\t * @param obj\n\t * @return\n\t */\n\tpublic static String toJSONString(Object obj) {\n\t\treturn toJSONString(obj, false);\n\t}\n\tpublic static String toJSONString(Object obj, boolean format) {\n\t\tif (obj == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (obj instanceof String) {\n\t\t\treturn (String) obj;\n\t\t}\n\n\t\t//if (obj instanceof Map) {\n\t\t//\t// Simple JSON object format\n\t\t//\tStringBuilder sb = new StringBuilder(\"{\");\n\t\t//\t@SuppressWarnings(\"unchecked\")\n\t\t//\tMap<Object, Object> map = (Map<Object, Object>) obj;\n\t\t//\tboolean first = true;\n\t\t//\tfor (Map.Entry<Object, Object> entry : map.entrySet()) {\n\t\t//\t\tif (! first) {\n\t\t//\t\t\tsb.append(\",\");\n\t\t//\t\t}\n\t\t//\n\t\t//\t\tfirst = false;\n\t\t//\t\tsb.append(\"\\\"\").append(entry.getKey()).append(\"\\\":\");\n\t\t//\t\tObject value = entry.getValue();\n\t\t//\t\tif (value instanceof String) {\n\t\t//\t\t\tsb.append(\"\\\"\").append(value).append(\"\\\"\");\n\t\t//\t\t} else {\n\t\t//\t\t\tsb.append(toJSONString(value));\n\t\t//\t\t}\n\t\t//\t}\n\t\t//\tsb.append(\"}\");\n\t\t//\treturn sb.toString();\n\t\t//}\n\t\t//\n\t\t//if (obj instanceof List) {\n\t\t//\tStringBuilder sb = new StringBuilder(\"[\");\n\t\t//\t@SuppressWarnings(\"unchecked\")\n\t\t//\tList<Object> list = (List<Object>) obj;\n\t\t//\tboolean first = true;\n\t\t//\tfor (Object item : list) {\n\t\t//\t\tif (! first) {\n\t\t//\t\t\t\tsb.append(\",\");\n\t\t//\t\t}\n\t\t//\t\tfirst = false;\n\t\t//\t\tif (item instanceof String) {\n\t\t//\t\t\tsb.append(\"\\\"\").append(item).append(\"\\\"\");\n\t\t//\t\t} else {\n\t\t//\t\t\tsb.append(toJSONString(item));\n\t\t//\t\t}\n\t\t//\t}\n\t\t//\tsb.append(\"]\");\n\t\t//\treturn sb.toString();\n\t\t//}\n\n\t\treturn DEFAULT_JSON_PARSER.toJSONString(obj, format);\n\t}\n\n\n\t/**判断是否为JSONObject或JSONArray的isXxx方法名\n\t * @param key\n\t * @return\n\t */\n\tpublic static boolean isJSONType(String key) {\n\t\treturn key != null && key.startsWith(\"is\") && key.length() > 2 && key.contains(\"JSON\");\n\t}\n\n\tpublic static boolean isBoolOrNumOrStr(Object obj) {\n\t\treturn obj instanceof Boolean || obj instanceof Number || obj instanceof String;\n\t}\n\n\t/**\n\t * Get a value from a Map and convert to the specified type\n\t * @param map Source map\n\t * @param key The key\n\t * @param <T> Target type\n\t * @return The converted value\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T get(Map<String, Object> map, String key) {\n\t\treturn map == null || key == null ? null : (T) map.get(key);\n\t}\n\n\t/**\n\t * Get a value from a Map and convert to the specified type\n\t * @param map Source map\n\t * @param key The key\n\t * @param <M> Target type\n\t * @return The converted value\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <M extends Map<String, Object>> M getJSONObject(Map<String, Object> map, String key) {\n\t\tObject obj = get(map, key);\n\t\treturn (M) obj;\n\t}\n\n\t/**\n\t * Get a value from a Map and convert to the specified type\n\t * @param map Source map\n\t * @param key The key\n\t * @param <L> Target type\n\t * @return The converted value\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <L extends List<Object>> L getJSONArray(Map<String, Object> map, String key) {\n\t\tObject obj = get(map, key);\n\t\treturn (L) obj;\n\t}\n\n\t/**\n\t * Get a value from a Map and convert to the specified type\n\t * @param list Source map\n\t * @param index The key\n\t * @param <T> Target type\n\t * @return The converted value\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> T get(List<Object> list, int index) {\n\t\treturn list == null || index < 0 || index >= list.size() ? null : (T) list.get(index);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <M extends Map<String, Object>> M getJSONObject(List<Object> list, int index) {\n\t\tObject obj = get(list, index);\n\t\treturn (M) obj;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <L extends List<Object>> L getJSONArray(List<Object> list, int index) {\n\t\tObject obj = get(list, index);\n\t\treturn (L) obj;\n\t}\n\n//\t/**\n//\t * Get a value from a Map and convert to the specified type\n//\t * @param map Source map\n//\t * @param key The key\n//\t * @param <T> Target type\n//\t * @return The converted value\n//\t */\n//\t@SuppressWarnings(\"unchecked\")\n//\tpublic static <T> T get(List<T> list, int index) {\n//\t\treturn list == null || index < 0 || index >= list.size() ? null : list.get(index);\n//\t}\n\n\t/**\n\t * Get a Map value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The Map value\n\t * @throws IllegalArgumentException If value is not a Map and cannot be converted\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <K, V> Map<K, V> getMap(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Map) {\n\t\t\treturn (Map<K, V>) value;\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Value for key '\" + key + \"' is not a Map: \" + value.getClass().getName());\n\t}\n\n\t/**\n\t * Get a List value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The List value\n\t * @throws IllegalArgumentException If value is not a List and cannot be converted\n\t */\n\t@SuppressWarnings(\"unchecked\")\n\tpublic static <T> List<T> getList(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof List) {\n\t\t\treturn (List<T>) value;\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Value for key '\" + key + \"' is not a List: \" + value.getClass().getName());\n\t}\n\n\t/**\n\t * Get an int value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The int value\n\t * @throws IllegalArgumentException If value cannot be converted to int\n\t */\n\tpublic static Integer getInteger(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).intValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to int: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to int\");\n\t}\n\n\t/**\n\t * Get an int value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The int value\n\t * @throws IllegalArgumentException If value cannot be converted to int\n\t */\n\tpublic static int getIntValue(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).intValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Integer.parseInt((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to int: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to int\");\n\t}\n\n\t/**\n\t * Get an int value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The int value\n\t * @throws IllegalArgumentException If value cannot be converted to int\n\t */\n\tpublic static Long getLong(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).longValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Long.parseLong((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to int: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to int\");\n\t}\n\n\t/**\n\t * Get a long value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The long value\n\t * @throws IllegalArgumentException If value cannot be converted to long\n\t */\n\tpublic static long getLongValue(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).longValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Long.parseLong((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to long: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to long\");\n\t}\n\n\t/**\n\t * Get a double value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The double value\n\t * @throws IllegalArgumentException If value cannot be converted to double\n\t */\n\tpublic static Float getFloat(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).floatValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Float.parseFloat((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to double: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to double\");\n\t}\n\n\t/**\n\t * Get a double value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The double value\n\t * @throws IllegalArgumentException If value cannot be converted to double\n\t */\n\tpublic static float getFloatValue(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).floatValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Float.parseFloat((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to double: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to double\");\n\t}\n\n\n\t/**\n\t * Get a double value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The double value\n\t * @throws IllegalArgumentException If value cannot be converted to double\n\t */\n\tpublic static Double getDouble(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).doubleValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Double.parseDouble((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to double: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to double\");\n\t}\n\n\t/**\n\t * Get a double value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The double value\n\t * @throws IllegalArgumentException If value cannot be converted to double\n\t */\n\tpublic static double getDoubleValue(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\treturn ((Number) value).doubleValue();\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\ttry {\n\t\t\t\treturn Double.parseDouble((String) value);\n\t\t\t} catch (NumberFormatException e) {\n\t\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to double: \" + e.getMessage());\n\t\t\t}\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to double\");\n\t}\n\n\n\t/**\n\t * Get a boolean value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The boolean value\n\t * @throws IllegalArgumentException If value cannot be converted to boolean\n\t */\n\tpublic static Boolean getBoolean(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (value instanceof Boolean) {\n\t\t\treturn (Boolean) value;\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\tString str = ((String) value).toLowerCase();\n\t\t\tif (str.equals(\"true\") || str.equals(\"false\")) {\n\t\t\t\treturn Boolean.parseBoolean(str);\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to boolean\");\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\tint intValue = ((Number) value).intValue();\n\t\t\tif (intValue == 0 || intValue == 1) {\n\t\t\t\treturn intValue != 0;\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Cannot convert Number value '\" + value + \"' to boolean. Only 0 and 1 are supported.\");\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to boolean\");\n\t}\n\n\t/**\n\t * Get a boolean value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The boolean value\n\t * @throws IllegalArgumentException If value cannot be converted to boolean\n\t */\n\tpublic static boolean getBooleanValue(Map<String, Object> map, String key) throws IllegalArgumentException {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (value instanceof Boolean) {\n\t\t\treturn (Boolean) value;\n\t\t}\n\n\t\tif (value instanceof String) {\n\t\t\tString str = ((String) value).toLowerCase();\n\t\t\tif (str.equals(\"true\") || str.equals(\"false\")) {\n\t\t\t\treturn Boolean.parseBoolean(str);\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Cannot convert String value '\" + value + \"' to boolean\");\n\t\t}\n\n\t\tif (value instanceof Number) {\n\t\t\tint intValue = ((Number) value).intValue();\n\t\t\tif (intValue == 0 || intValue == 1) {\n\t\t\t\treturn intValue != 0;\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(\"Cannot convert Number value '\" + value + \"' to boolean. Only 0 and 1 are supported.\");\n\t\t}\n\n\t\tthrow new IllegalArgumentException(\"Cannot convert value of type \" + value.getClass().getName() + \" to boolean\");\n\t}\n\n\t/**\n\t * Get a string value from a Map\n\t * @param map Source map\n\t * @param key The key\n\t * @return The string value\n\t */\n\tpublic static String getString(Map<String, Object> map, String key) {\n\t\tObject value = map == null || key == null ? null : map.get(key);\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn value.toString();\n\t}\n\n\n\tpublic static Object getFromObjOrArr(Object parent, String k) {\n\t\tif (parent instanceof Map<?, ?>) {\n\t\t\treturn ((Map<String, Object>) parent).get(k);\n\t\t}\n\n\t\tif (parent instanceof List<?>) {\n\t\t\tint j = Integer.valueOf(k);\n\t\t\treturn ((List<?>) parent).get(j);\n\t\t}\n\n\t\treturn null;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONCreator.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.Collection;\nimport java.util.List;\nimport java.util.Map;\n\n/**JSON相关创建器\n * @author Lemon\n */\npublic interface JSONCreator<M extends Map<String, Object>, L extends List<Object>> {\n\t\n\t@NotNull\n    M createJSONObject();\n\n    @NotNull\n    default M createJSONObject(String key, Object value) {\n        M obj = createJSONObject();\n        obj.put(key, value);\n        return obj;\n    }\n\n    @NotNull\n    default M createJSONObject(Map<? extends String, ?> map) {\n        M obj = createJSONObject();\n        if (map != null && ! map.isEmpty()) {\n            obj.putAll(map);\n        }\n        return obj;\n    }\n\n\t@NotNull\n    L createJSONArray();\n\n    @NotNull\n    default L createJSONArray(Object obj){\n        L arr = createJSONArray();\n        arr.add(obj);\n        return arr;\n    }\n\n    @NotNull\n    default L createJSONArray(Collection<?> list){\n        L arr = createJSONArray();\n        if (list != null && ! list.isEmpty()) {\n            arr.addAll(list);\n        }\n        return arr;\n    }\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONList.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\npackage apijson;\n\nimport java.util.*;\n\n/**\n * Custom JSONList implementation based on ArrayList to replace com.alibaba.fastjson.JSONList\n * Maintains same API as fastjson but uses standard Java List implementation\n * @author Lemon\n */\npublic interface JSONList<M extends Map<String, Object>, L extends List<Object>> extends List<Object> {\n    public static final String TAG = \"JSONList\";\n\n    ///**\n    // * Create an empty JSONList\n    // */\n    //default JSONList() {\n    //    super();\n    //}\n    //\n    //private int initialCapacity = 10;\n    ///**\n    // * Create a JSONList with initial capacity\n    // * @param initialCapacity the initial capacity\n    // */\n    //default JSONList(int initialCapacity) {\n    //    super(initialCapacity);\n    //}\n    //\n    ///**\n    // * Create a JSONList from a Collection\n    // * @param collection the collection to copy from\n    // */\n    //default JSONList(Collection<?> collection) {\n    //    super(collection);\n    //}\n    //\n    ///**\n    // * Create a JSONList from a JSON string\n    // * @param json JSON string\n    // */\n    //default JSONList(String json) {\n    //    this();\n    //    List<Object> list = JSON.parseArray(json);\n    //    if (list != null) {\n    //        addAll(list);\n    //    }\n    //}\n    //\n    /**\n     * Get a JSONMap at the specified index\n     * @param index the index\n     * @return the JSONMap or null if not a JSONMap\n     */\n    default M getJSONObject(int index) {\n        if (index < 0 || index >= size()) {\n            return null;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof Map<?, ?>) {\n            return JSON.createJSONObject((Map<? extends String, ?>) obj);\n        }\n\n        return null;\n    }\n    \n    /**\n     * Get a JSONList at the specified index\n     * @param index the index\n     * @return the JSONList or null if not a JSONList\n     */\n    default L getJSONArray(int index) {\n        if (index < 0 || index >= size()) {\n            return null;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof List<?>) {\n            return JSON.createJSONArray((List<?>) obj);\n        }\n\n        return null;\n    }\n    \n    /**\n     * Get a boolean value at the specified index\n     * @param index the index\n     * @return the boolean value or false if not found\n     */\n    default boolean getBooleanValue(int index) {\n        if (index < 0 || index >= size()) {\n            return false;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof Boolean) {\n            return (Boolean) obj;\n        } else if (obj instanceof Number) {\n            return ((Number) obj).intValue() != 0;\n        } else if (obj instanceof String) {\n            return Boolean.parseBoolean((String) obj);\n        }\n\n        return false;\n    }\n    \n    /**\n     * Get an integer value at the specified index\n     * @param index the index\n     * @return the integer value or 0 if not found\n     */\n    default int getIntValue(int index) {\n        if (index < 0 || index >= size()) {\n            return 0;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof Number) {\n            return ((Number) obj).intValue();\n        } else if (obj instanceof String) {\n            try {\n                return Integer.parseInt((String) obj);\n            } catch (NumberFormatException e) {\n                // Ignore\n            }\n        }\n        return 0;\n    }\n    \n    /**\n     * Get a long value at the specified index\n     * @param index the index\n     * @return the long value or 0 if not found\n     */\n    default long getLongValue(int index) {\n        if (index < 0 || index >= size()) {\n            return 0L;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof Number) {\n            return ((Number) obj).longValue();\n        } else if (obj instanceof String) {\n            try {\n                return Long.parseLong((String) obj);\n            } catch (NumberFormatException e) {\n                // Ignore\n            }\n        }\n        return 0L;\n    }\n    \n    /**\n     * Get a double value at the specified index\n     * @param index the index\n     * @return the double value or 0 if not found\n     */\n    default double getDoubleValue(int index) {\n        if (index < 0 || index >= size()) {\n            return 0.0;\n        }\n        \n        Object obj = get(index);\n        if (obj instanceof Number) {\n            return ((Number) obj).doubleValue();\n        } else if (obj instanceof String) {\n            try {\n                return Double.parseDouble((String) obj);\n            } catch (NumberFormatException e) {\n                // Ignore\n            }\n        }\n        return 0.0;\n    }\n    \n    /**\n     * Get a string value at the specified index\n     * @param index the index\n     * @return the string value or null if not found\n     */\n    default String getString(int index) {\n        if (index < 0 || index >= size()) {\n            return null;\n        }\n        \n        Object obj = get(index);\n        return obj != null ? obj.toString() : null;\n    }\n\n    \n    default String toJSONString() {\n        return JSON.toJSONString(this);\n    }\n\n    //@Override\n    //default boolean containsAll(Collection<?> c) {\n    //    if (c == null || c.isEmpty()) {\n    //        return true;\n    //    }\n    //    return super.containsAll(c);\n    //}\n    //\n    //@Override\n    //default boolean addAll(Collection<?> c) {\n    //    if (c == null || c.isEmpty()) {\n    //        return true;\n    //    }\n    //    return super.addAll(c);\n    //}\n    //\n    //@Override\n    //default boolean addAll(int index, Collection<?> c) {\n    //    if (c == null || c.isEmpty()) {\n    //        return true;\n    //    }\n    //\n    //    int sz = size();\n    //    if (index < 0 || index >= sz) {\n    //        index += sz;\n    //    }\n    //\n    //    return super.addAll(index, c);\n    //}\n    //\n    //@Override\n    //default boolean removeAll(Collection<?> c) {\n    //    if (c == null || c.isEmpty()) {\n    //        return true;\n    //    }\n    //    return super.removeAll(c);\n    //}\n    //\n    //@Override\n    //default boolean retainAll(Collection<?> c) {\n    //    if (c == null || c.isEmpty()) {\n    //        return true;\n    //    }\n    //    return super.retainAll(c);\n    //}\n    //\n    //\n    //@Override\n    //default Object get(int index) {\n    //    int sz = size();\n    //    if (index < 0 || index >= sz) {\n    //        index += sz;\n    //    }\n    //\n    //    return super.get(index);\n    //}\n    //\n    //@Override\n    //default Object set(int index, Object element) {\n    //    int sz = size();\n    //    if (index < 0 || index >= sz) {\n    //        index += sz;\n    //    }\n    //\n    //    return super.set(index, element);\n    //}\n    //\n    //@Override\n    //default void add(int index, Object element) {\n    //    int sz = size();\n    //    if (index < 0 || index >= sz) {\n    //        index += sz;\n    //    }\n    //\n    //    super.add(index, element);\n    //}\n    //\n    //@Override\n    //default Object remove(int index) {\n    //    int sz = size();\n    //    if (index < 0 && index >= -sz) {\n    //        index += sz;\n    //    }\n    //    if (index < 0 || index >= sz) {\n    //        return null;\n    //    }\n    //\n    //    return super.remove(index);\n    //}\n    //\n    //@Override\n    //default ListIterator<Object> listIterator(int index) {\n    //    int sz = size();\n    //    if (index < 0 && index >= -sz) {\n    //        index += sz;\n    //    }\n    //\n    //    return super.listIterator(index);\n    //}\n    //\n    //@Override\n    //default List<Object> subList(int fromIndex, int toIndex) {\n    //    int sz = size();\n    //    if (fromIndex < 0 && fromIndex >= -sz) {\n    //        fromIndex += sz;\n    //    }\n    //    if (toIndex < 0 && toIndex >= -sz) {\n    //        toIndex += sz;\n    //    }\n    //\n    //    return super.subList(fromIndex, toIndex);\n    //}\n\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONMap.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.*;\n\n\n/**use this class instead of com.alibaba.fastjson.JSONMap\n * @author Lemon\n * @see #put\n * @see #puts\n * @see #putsAll\n */\n//default class JSONMap<M, L> extends LinkedHashMap<String, Object> {\npublic interface JSONMap<M extends Map<String, Object>, L extends List<Object>> extends Map<String, Object> {\n\tstatic final String TAG = \"JSONMap\";\n\n\t// 只能是 static public Map<String, Object> map = new LinkedHashMap<>();\n\n\t///**ordered\n\t// */\n\t//default JSONMap() {\n\t//\tsuper();\n\t//}\n\t///**transfer Object to JSONMap\n\t// * @param object\n\t// * @see {@link #JSONMap(Object)}\n\t// */\n\t//default JSONMap(Object object) {\n\t//\tthis();\n\t//\tif (object instanceof Map) {\n\t//\t\t@SuppressWarnings(\"unchecked\")\n\t//\t\tMap<String, Object> map = (Map<String, Object>) object;\n\t//\t\tputAll(map);\n\t//\t} else if (object != null) {\n\t//\t\tString json = JSON.toJSONString(object);\n\t//\t\tif (json != null) {\n\t//\t\t\tMap<String, Object> map = JSON.parseObject(json);\n\t//\t\t\tif (map != null) {\n\t//\t\t\t\tputAll(map);\n\t//\t\t\t}\n\t//\t\t}\n\t//\t}\n\t//}\n\t///**parse JSONMap<M, L> with JSON String\n\t// * @param json\n\t// * @see {@link #JSONMap(String)}\n\t// */\n\t//default JSONMap(String json) {\n\t//\tthis();\n\t//\tMap<String, Object> map = JSON.parseObject(json);\n\t//\tif (map != null) {\n\t//\t\tputAll(map);\n\t//\t}\n\t//}\n\t///**transfer com.alibaba.fastjson.JSONMap<M, L> to JSONMap\n\t// * @param object\n\t// * @see {@link #putsAll(Map<? extends String, ? extends Object>)}\n\t// */\n\t//default JSONMap(Map<String, Object> object) {\n\t//\tthis();\n\t//\tputsAll(object);\n\t//}\n\n\t//public static JSONMap<M, L> valueOf(Object obj) {\n\t//\tJSONMap<M, L> req = new JSONMap() {};\n\t//\tMap<String, Object> m = JSON.parseObject(obj);\n\t//\tif (m != null && ! m.isEmpty()) {\n\t//\t\treq.map.putAll(m);\n\t//\t}\n\t//\treturn req;\n\t//}\n\n\t//judge <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\tString KEY_ARRAY = \"[]\";\n\n\t/**判断是否为Array的key\n\t * @param key\n\t * @return\n\t */\n\tpublic static boolean isArrayKey(String key) {\n\t\treturn key != null && key.endsWith(KEY_ARRAY);\n\t}\n\t/**判断是否为对应Table的key\n\t * @param key\n\t * @return\n\t */\n\tpublic static boolean isTableKey(String key) {\n\t\treturn StringUtil.isBigName(key);\n\t}\n\t/**判断是否为对应Table数组的 key\n\t * @param key\n\t * @return\n\t */\n\tpublic static boolean isTableArray(String key) {\n\t\treturn isArrayKey(key) && isTableKey(key.substring(0, key.length() - KEY_ARRAY.length()));\n\t}\n\t//judge >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//JSONObject内关键词 key <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\n\tpublic static String KEY_ID = \"id\";\n\tpublic static String KEY_ID_IN = KEY_ID + \"{}\";\n\tpublic static String KEY_USER_ID = \"userId\";\n\tpublic static String KEY_USER_ID_IN = KEY_USER_ID + \"{}\";\n\n\tdefault String getIdKey() {\n\t\treturn KEY_ID;\n\t}\n\tdefault String getIdInKey() {\n\t\treturn KEY_ID_IN;\n\t}\n\tdefault String getUserIdKey() {\n\t\treturn KEY_USER_ID;\n\t}\n\tdefault String getUserIdInKey() {\n\t\treturn KEY_USER_ID_IN;\n\t}\n\n\t/**set \"id\":id in Table layer\n\t * @param id\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setId(Long id) {\n\t\treturn puts(getIdKey(), id);\n\t}\n\t/**set \"id{}\":[] in Table layer\n\t * @param list\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setIdIn(List<Object> list) {\n\t\treturn puts(getIdInKey(), list);\n\t}\n\n\t/**set \"userId\":userId in Table layer\n\t * @param id\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setUserId(Long id) {\n\t\treturn puts(getUserIdKey(), id);\n\t}\n\t/**set \"userId{}\":[] in Table layer\n\t * @param list\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setUserIdIn(List<Object> list) {\n\t\treturn puts(getUserIdInKey(), list);\n\t}\n\n\n\tint CACHE_ALL = 0;\n\tint CACHE_ROM = 1;\n\tint CACHE_RAM = 2;\n\n\tString CACHE_ALL_STRING = \"ALL\";\n\tString CACHE_ROM_STRING = \"ROM\";\n\tString CACHE_RAM_STRING = \"RAM\";\n\n\n\t//@key关键字都放这个类 <<<<<<<<<<<<<<<<<<<<<<\n\tString KEY_TRY = \"@try\"; // 尝试，忽略异常\n\tString KEY_CATCH = \"@catch\"; // TODO 捕捉到异常后，处理方式  null-不处理；DEFAULT-返回默认值；ORIGIN-返回请求里的原始值\n\tString KEY_DROP = \"@drop\"; // 丢弃，不返回，TODO 应该通过 fastjson 的 ignore 之类的机制来处理，避免导致下面的对象也不返回\n\t//\tString KEY_KEEP = \"@keep\"; // 一定会返回，为 null 或 空对象时，会使用默认值(非空)，解决其它对象因为不关联的第一个对为空导致也不返回\n\tString KEY_DEFAULT = \"@default\"; // TODO 自定义默认值 { \"@default\":true }，@default 可完全替代 @keep\n\tString KEY_NULL = \"@null\"; // 值为 null 的键值对 \"@null\":\"tag,pictureList\"，允许 is NULL 条件判断， SET tag = NULL 修改值为 NULL 等\n\tString KEY_CAST = \"@cast\"; // 类型转换 cast(date AS DATE)\n\n\tString KEY_ROLE = \"@role\"; // 角色，拥有对某些数据的某些操作的权限\n\tString KEY_DATABASE = \"@database\"; // 数据库类型，默认为MySQL\n\tString KEY_DATASOURCE = \"@datasource\"; // 数据源\n\tString KEY_NAMESPACE = \"@namespace\"; // 命名空间，Table 在非默认 namespace 内时需要声明\n\tString KEY_CATALOG = \"@catalog\"; // 目录，Table 在非默认 catalog 内时需要声明\n\tString KEY_SCHEMA = \"@schema\"; // 数据库，Table 在非默认 schema 内时需要声明\n\tString KEY_EXPLAIN = \"@explain\"; // 分析 true/false\n\tString KEY_CACHE = \"@cache\"; // 缓存 RAM/ROM/ALL\n\tString KEY_COLUMN = \"@column\"; // 查询的 Table 字段或 SQL 函数\n\tString KEY_FROM = \"@from\"; // FROM语句\n\tString KEY_COMBINE = \"@combine\"; // 条件组合，每个条件 key 前面可以放 &,|,! 逻辑关系  \"id!{},&sex,!name&$\"\n\tString KEY_GROUP = \"@group\"; // 分组方式\n\tString KEY_HAVING = \"@having\"; // 聚合函数条件，一般和 @group 一起用\n\tString KEY_HAVING_AND = \"@having&\"; // 聚合函数条件，一般和 @group 一起用\n\tString KEY_SAMPLE = \"@sample\"; // 取样方式\n\tString KEY_LATEST = \"@latest\"; // 最近方式\n\tString KEY_PARTITION = \"@partition\"; // 分区方式\n\tString KEY_FILL = \"@fill\"; // 填充方式\n\tString KEY_ORDER = \"@order\"; // 排序方式\n\tString KEY_KEY = \"@key\"; // key 映射，year:left(date,4);name_tag:(name,tag)\n\tString KEY_RAW = \"@raw\"; // 自定义原始 SQL 片段\n\tString KEY_JSON = \"@json\"; // 把字段转为 JSON 输出\n\tString KEY_STRING = \"@string\"; // 把字段转为 String 输入\n\tString KEY_TRIM = \"@trim\"; // 去除首位空格等空白字符\n\tString KEY_METHOD = \"@method\"; // json 对象配置操作方法\n\tString KEY_GET = \"@get\"; // json 对象配置操作方法\n\tString KEY_GETS = \"@gets\"; // json 对象配置操作方法\n\tString KEY_HEAD = \"@head\"; // json 对象配置操作方法\n\tString KEY_HEADS = \"@heads\"; // json 对象配置操作方法\n\tString KEY_POST = \"@post\"; // json 对象配置操作方法\n\tString KEY_PUT = \"@put\"; // json 对象配置操作方法\n\tString KEY_DELETE = \"@delete\"; // json 对象配置操作方法\n\n\tList<String> TABLE_KEY_LIST = new ArrayList<>(Arrays.asList(\n\t\t\tKEY_ROLE, \n\t\t\tKEY_DATABASE, \n\t\t\tKEY_DATASOURCE,\n\t\t\tKEY_NAMESPACE,\n\t\t\tKEY_CATALOG,\n\t\t\tKEY_SCHEMA,\n\t\t\tKEY_EXPLAIN,\n\t\t\tKEY_CACHE,\n\t\t\tKEY_COLUMN,\n\t\t\tKEY_FROM,\n\t\t\tKEY_NULL,\n\t\t\tKEY_CAST,\n\t\t\tKEY_COMBINE,\n\t\t\tKEY_GROUP,\n\t\t\tKEY_HAVING,\n\t\t\tKEY_HAVING_AND,\n\t\t\tKEY_SAMPLE,\n\t\t\tKEY_LATEST,\n\t\t\tKEY_PARTITION,\n\t\t\tKEY_FILL,\n\t\t\tKEY_ORDER,\n\t\t\tKEY_KEY,\n\t\t\tKEY_RAW,\n\t\t\tKEY_JSON,\n\t\t\tKEY_STRING,\n\t\t\tKEY_TRIM,\n\t\t\tKEY_METHOD,\n\t\t\tKEY_GET,\n\t\t\tKEY_GETS,\n\t\t\tKEY_HEAD,\n\t\t\tKEY_HEADS,\n\t\t\tKEY_POST,\n\t\t\tKEY_PUT,\n\t\t\tKEY_DELETE\n\t));\n\n\t//@key关键字都放这个类 >>>>>>>>>>>>>>>>>>>>>>\n\n\n\t/**set try, ignore exceptions\n\t * @param tri\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setTry(Boolean tri) {\n\t\treturn puts(KEY_TRY, tri);\n\t}\n\n\t/**set catch\n\t * @param isCatch\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setCatch(String isCatch) {\n\t\treturn puts(KEY_CATCH, isCatch);\n\t}\n\t/**set drop, data dropped will not return\n\t * @param drop\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setDrop(Boolean drop) {\n\t\treturn puts(KEY_DROP, drop);\n\t}\n\n\t/**set if has default\n\t * @param hasDefault\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setDefault(Boolean hasDefault) {\n\t\treturn puts(KEY_DEFAULT, hasDefault);\n\t}\n\n\n\t/**set role of request sender\n\t * @param role\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setRole(String role) {\n\t\treturn puts(KEY_ROLE, role);\n\t}\n\t/**set database where table was puts\n\t * @param database\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setDatabase(String database) {\n\t\treturn puts(KEY_DATABASE, database);\n\t}\n\t/**set datasource where table was puts\n\t * @param datasource\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setDatasource(String datasource) {\n\t\treturn puts(KEY_DATASOURCE, datasource);\n\t}\n\t/**set namespace where table was puts\n\t * @param namespace\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setNamespace(String namespace) {\n\t\treturn puts(KEY_NAMESPACE, namespace);\n\t}\n\t/**set catalog where table was puts\n\t * @param catalog\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setCatalog(String catalog) {\n\t\treturn puts(KEY_CATALOG, catalog);\n\t}\n\t/**set schema where table was puts\n\t * @param schema\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> setSchema(String schema) {\n\t\treturn puts(KEY_SCHEMA, schema);\n\t}\n\t/**set if return explain informations\n\t * @param explain\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setExplain(Boolean explain) {\n\t\treturn puts(KEY_EXPLAIN, explain);\n\t}\n\t/**set cache type\n\t * @param cache\n\t * @return\n\t * @see {@link #CACHE_ALL}\n\t * @see {@link #CACHE_RAM}\n\t * @see {@link #CACHE_ROM}\n\t */\n\tdefault JSONMap<M, L> setCache(Integer cache) {\n\t\treturn puts(KEY_CACHE, cache);\n\t}\n\t/**set cache type\n\t * @param cache\n\t * @return\n\t * @see {@link #CACHE_ALL_STRING}\n\t * @see {@link #CACHE_RAM_STRING}\n\t * @see {@link #CACHE_ROM_STRING}\n\t */\n\tdefault JSONMap<M, L> setCache(String cache) {\n\t\treturn puts(KEY_CACHE, cache);\n\t}\n\n\t/**set keys need to be returned\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setColumn(String)}\n\t */\n\tdefault JSONMap<M, L> setColumn(String... keys) {\n\t\treturn setColumn(StringUtil.get(keys, true));\n\t}\n\t/**set keys need to be returned\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setColumn(String keys) {\n\t\treturn puts(KEY_COLUMN, keys);\n\t}\n\n\t/**set keys whose value is null\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setNull(String)}\n\t */\n\tdefault JSONMap<M, L> setNull(String... keys) {\n\t\treturn setNull(StringUtil.get(keys, true));\n\t}\n\t/**set keys whose value is null\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setNull(String keys) {\n\t\treturn puts(KEY_NULL, keys);\n\t}\n\t\n\t/**set keys and types whose value should be cast to type, cast(value AS DATE)\n\t * @param keyTypes  key0:type0, key1:type1, key2:type2 ...\n\t * @return {@link #setCast(String)}\n\t */\n\tdefault JSONMap<M, L> setCast(String... keyTypes) {\n\t\treturn setCast(StringUtil.get(keyTypes, true));\n\t}\n\t/**set keys and types whose value should be cast to type, cast(value AS DATE)\n\t * @param keyTypes  \"key0:type0,key1:type1,key2:type2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setCast(String keyTypes) {\n\t\treturn puts(KEY_CAST, keyTypes);\n\t}\n\n\t/**set combination of keys for conditions\n\t * @param keys  key0,&key1,|key2,!key3 ...  TODO or key0> | (key1{} & !key2)...\n\t * @return {@link #setColumn(String)}\n\t */\n\tdefault JSONMap<M, L> setCombine(String... keys) {\n\t\treturn setCombine(StringUtil.get(keys, true));\n\t}\n\t/**set combination of keys for conditions\n\t * @param keys  key0,&key1,|key2,!key3 ...  TODO or key0> | (key1{} & !key2)...\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setCombine(String keys) {\n\t\treturn puts(KEY_COMBINE, keys);\n\t}\n\n\t/**set keys for group by\n\t * @param keys key0, key1, key2 ...\n\t * @return {@link #setGroup(String)}\n\t */\n\tdefault JSONMap<M, L> setGroup(String... keys) {\n\t\treturn setGroup(StringUtil.get(keys, true));\n\t}\n\t/**set keys for group by\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setGroup(String keys) {\n\t\treturn puts(KEY_GROUP, keys);\n\t}\n\n\t/**set keys for having\n\t * @param keys count(key0) > 1, sum(key1) <= 5, function2(key2) ? value2 ...\n\t * @return {@link #setHaving(String)}\n\t */\n\tdefault JSONMap<M, L> setHaving(String... keys) {\n\t\treturn setHaving(StringUtil.get(keys, true));\n\t}\n\t/**set keys for having\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setHaving(String keys) {\n\t\treturn setHaving(keys, false);\n\t}\n\t/**set keys for having\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setHaving(String keys, boolean isAnd) {\n\t\treturn puts(isAnd ? KEY_HAVING_AND : KEY_HAVING, keys);\n\t}\n\n\t/**set keys for sample by\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setSample(String)}\n\t */\n\tdefault JSONMap<M, L> setSample(String... keys) {\n\t\treturn setSample(StringUtil.get(keys, true));\n\t}\n\t/**set keys for sample by\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setSample(String keys) {\n\t\treturn puts(KEY_SAMPLE, keys);\n\t}\n\n\t/**set keys for latest on\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setLatest(String)}\n\t */\n\tdefault JSONMap<M, L> setLatest(String... keys) {\n\t\treturn setLatest(StringUtil.get(keys, true));\n\t}\n\t/**set keys for latest on\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setLatest(String keys) {\n\t\treturn puts(KEY_LATEST, keys);\n\t}\n\n\t/**set keys for partition by\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setPartition(String)}\n\t */\n\tdefault JSONMap<M, L> setPartition(String... keys) {\n\t\treturn setPartition(StringUtil.get(keys, true));\n\t}\n\t/**set keys for partition by\n\t * @param keys  key0, key1, key2 ...\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setPartition(String keys) {\n\t\treturn puts(KEY_PARTITION, keys);\n\t}\n\n\t/**set keys for fill(key): fill(null), fill(linear), fill(prev)\n\t * @param keys  key0, key1, key2 ...\n\t * @return {@link #setFill(String)}\n\t */\n\tdefault JSONMap<M, L> setFill(String... keys) {\n\t\treturn setFill(StringUtil.get(keys, true));\n\t}\n\t/**set keys for fill(key): fill(null), fill(linear), fill(prev)\n\t * @param keys  key0, key1, key2 ...\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setFill(String keys) {\n\t\treturn puts(KEY_FILL, keys);\n\t}\n\n\t/**set keys for order by\n\t * @param keys  key0, key1+, key2- ...\n\t * @return {@link #setOrder(String)}\n\t */\n\tdefault JSONMap<M, L> setOrder(String... keys) {\n\t\treturn setOrder(StringUtil.get(keys, true));\n\t}\n\t/**set keys for order by\n\t * @param keys  \"key0,key1+,key2-...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setOrder(String keys) {\n\t\treturn puts(KEY_ORDER, keys);\n\t}\n\n\t/**set key map\n\t * @param keyMap  \"name_tag:(name,tag);year:left(date,1,5)...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setKey(String keyMap) {\n\t\treturn puts(KEY_KEY, keyMap);\n\t}\n\n\t/**set keys to raw\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setRaw(String keys) {\n\t\treturn puts(KEY_RAW, keys);\n\t}\n\n\t/**set keys to cast to json\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setJson(String keys) {\n\t\treturn puts(KEY_JSON, keys);\n\t}\n\n\t/**set keys to cast to string\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setString(String keys) {\n\t\treturn puts(KEY_STRING, keys);\n\t}\n\n\t/**set keys to cast to string\n\t * @param keys  \"key0,key1,key2...\"\n\t * @return\n\t */\n\tdefault JSONMap<M, L> setTrim(String keys) {\n\t\treturn puts(KEY_TRIM, keys);\n\t}\n\n\t//JSONObject内关键词 key >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//Request <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\n\t/**\n\t * @param key\n\t * @param keys  path = keys[0] + \"/\" + keys[1] + \"/\" + keys[2] + ...\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsPath(String key, String... keys) {\n\t\treturn puts(key+\"@\", StringUtil.get(keys, \"/\"));\n\t}\n\n\t/**\n\t * @param key\n\t * @param isNull\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsNull(String key, boolean isNull) {\n\t\treturn puts(key+\"{}\", SQL.isNull(isNull));\n\t}\n\t/**\n\t * trim = false\n\t * @param key\n\t * @param isEmpty\n\t * @return {@link #putsEmpty(String, boolean, boolean)}\n\t */\n\tdefault JSONMap<M, L> putsEmpty(String key, boolean isEmpty) {\n\t\treturn putsEmpty(key, isEmpty, false);\n\t}\n\t/**\n\t * @param key\n\t * @param isEmpty\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsEmpty(String key, boolean isEmpty, boolean trim) {\n\t\treturn puts(key+\"{}\", SQL.isEmpty(key, isEmpty, trim));\n\t}\n\t/**\n\t * @param key\n\t * @param compare <=0, >5 ...\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsLength(String key, String compare) {\n\t\treturn puts(key+\"{}\", SQL.length(key) + compare);\n\t}\n\t/**\n\t * @param key\n\t * @param compare <=, > ...\n\t * @param value 1, 5, 3.14, -99 ...\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsLength(String key, String compare, Object value) {\n\t\treturn puts(key+\"[\"+(StringUtil.isEmpty(compare) || \"=\".equals(compare) ? \"\" : (\"!=\".equals(compare) ? \"!\" : compare)), value);\n\t}\n\t/**\n\t * @param key\n\t * @param compare <=0, >5 ...\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsJSONLength(String key, String compare) {\n\t\treturn puts(key+\"{}\", SQL.json_length(key) + compare);\n\t}\n\t/**\n\t * @param key\n\t * @param compare <=0, >5 ...\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsJSONLength(String key, String compare, Object value) {\n\t\treturn puts(key + \"{\" + (StringUtil.isEmpty(compare) || \"=\".equals(compare) ? \"\" : (\"!=\".equals(compare) ? \"!\" : compare)), value);\n\t}\n\n\t/**设置搜索\n\t * type = SEARCH_TYPE_CONTAIN_FULL\n\t * @param key\n\t * @param value\n\t * @return {@link #putsSearch(String, String, int)}\n\t */\n\tdefault JSONMap<M, L> putsSearch(String key, String value) {\n\t\treturn putsSearch(key, value, SQL.SEARCH_TYPE_CONTAIN_FULL);\n\t}\n\t/**设置搜索\n\t * @param key\n\t * @param value\n\t * @param type\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> putsSearch(String key, String value, int type) {\n\t\treturn puts(key+\"$\", SQL.search(value, type));\n\t}\n\n\t//Request >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t/**put and return this\n\t * @param value  must be annotated by {@link MethodAccess}\n\t * @return {@link #puts(String, Object)}\n\t */\n\tdefault JSONMap<M, L> puts(Object value) {\n\t\tput(value);\n\t\treturn this;\n\t}\n\t/**put and return this\n\t * @param key\n\t * @param value \n\t * @return this\n\t */\n\tdefault JSONMap<M, L> puts(String key, Object value) {\n\t\tput(key, value);\n\t\treturn this;\n\t}\n\n\t/**put and return value\n\t * @param value  must be annotated by {@link MethodAccess}\n\t */\n\tdefault Object put(Object value) {\n\t\tClass<?> clazz = value.getClass(); //should not return null\n\t\tif (clazz.getAnnotation(MethodAccess.class) == null) {\n\t\t\tthrow new IllegalArgumentException(\"puts  StringUtil.isEmpty(key, true)\" +\n\t\t\t\t\t\" clazz.getAnnotation(MethodAccess.class) == null\" +\n\t\t\t\t\t\" \\n key为空时仅支持 类型被@MethodAccess注解 的value !!!\" +\n\t\t\t\t\t\" \\n 如果一定要这么用，请对 \" + clazz.getName() + \" 注解！\" +\n\t\t\t\t\t\" \\n 如果是类似 key[]:{} 结构的请求，建议用 putsAll(...) ！\");\n\t\t}\n\t\treturn put(clazz.getSimpleName(), value);\n\t}\n\n\t/**puts key-value in object into this\n\t * @param map\n\t * @return this\n\t */\n\tdefault JSONMap<M, L> putsAll(Map<? extends String, ? extends Object> map) {\n\t\tputAll(map);\n\t\treturn this;\n\t}\n\n\n\t/**\n\t * Get a boolean value from the JSONMap\n\t * @param key the key\n\t * @return the boolean value or false if not found\n\t */\n\tdefault boolean getBooleanValue(String key) {\n\t\treturn JSON.getBooleanValue(this, key);\n\t}\n\t\n\t/**\n\t * Get an integer value from the JSONMap\n\t * @param key the key\n\t * @return the integer value or 0 if not found\n\t */\n\tdefault int getIntValue(String key) {\n\t\treturn JSON.getIntValue(this, key);\n\t}\n\t\n\t/**\n\t * Get a long value from the JSONMap\n\t * @param key the key\n\t * @return the long value or 0 if not found\n\t */\n\tdefault long getLongValue(String key) {\n\t\treturn JSON.getLongValue(this, key);\n\t}\n\t\n\t/**\n\t * Get a double value from the JSONMap\n\t * @param key the key\n\t * @return the double value or 0 if not found\n\t */\n\tdefault double getDoubleValue(String key) {\n\t\treturn JSON.getDoubleValue(this, key);\n\t}\n\t\n\t/**\n\t * Get a string value from the JSONMap\n\t * @param key the key\n\t * @return the string value or null if not found\n\t */\n\tdefault String getString(String key) {\n\t\tObject value = get(key);\n\t\treturn value != null ? value.toString() : null;\n\t}\n\t\n\t/**\n\t * Get a JSONMap<M, L> value from the JSONMap\n\t * @param key the key\n\t * @return the JSONMap<M, L> value or null if not found\n\t */\n\tdefault M getJSONObject(String key) {\n\t\tMap<String, Object> map = JSON.getMap(this, key);\n\t\treturn map != null ? JSON.createJSONObject(map) : null;\n\t}\n\n\t/**\n\t * Get a JSONList value from the JSONMap\n\t * @param key the key\n\t * @return the JSONList value or null if not found\n\t */\n\tdefault L getJSONArray(String key) {\n\t\tList<Object> list = JSON.getList(this, key);\n\t\treturn list != null ? JSON.createJSONArray(list) : null;\n\t}\n\n\t@Override\n\tdefault void putAll(Map<? extends String, ? extends Object> map) {\n\t\tSet<? extends Map.Entry<? extends String, ?>> set = map == null ? null : map.entrySet();\n\t\tif (set != null || set.isEmpty()) {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (Map.Entry<? extends String, ?> entry : set) {\n\t\t\tput(entry.getKey(), entry.getValue());\n\t\t}\n\t}\n\n\tdefault String toJSONString() {\n\t\treturn JSON.toJSONString(this);\n\t}\n\n\t//@Override\n\t//default int size() {\n\t//\treturn map.size();\n\t//}\n\t//\n\t//@Override\n\t//default boolean isEmpty() {\n\t//\treturn map.isEmpty();\n\t//}\n\t//\n\t//@Override\n\t//default boolean containsKey(Object key) {\n\t//\treturn map.containsKey(key);\n\t//}\n\t//\n\t//@Override\n\t//default boolean containsValue(Object value) {\n\t//\treturn map.containsValue(value);\n\t//}\n\t//\n\t//@Override\n\t//default Object get(Object key) {\n\t//\treturn map.get(key);\n\t//}\n\t//\n\t//@Override\n\t//default Object put(String key, Object value) {\n\t//\treturn map.put(key, value);\n\t//}\n\t//\n\t//@Override\n\t//default Object remove(Object key) {\n\t//\treturn map.remove(key);\n\t//}\n\n\n\t//@Override\n\t//default void clear() {\n\t//\tmap.clear();\n\t//}\n\t//\n\t//@Override\n\t//default Set<String> keySet() {\n\t//\treturn map.keySet();\n\t//}\n\t//\n\t//@Override\n\t//default Collection<Object> values() {\n\t//\treturn map.values();\n\t//}\n\t//\n\t//@Override\n\t//default Set<Entry<String, Object>> entrySet() {\n\t//\treturn map.entrySet();\n\t//}\n\n\t//@Override\n\t//default String toString() {\n\t//\treturn JSON.toJSONString(this);\n\t//}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**JSON 相关解析器\n * @author Lemon\n */\npublic interface JSONParser<M extends Map<String, Object>, L extends List<Object>> extends JSONCreator<M, L> {\n\n    Object parse(Object json);\n\n    M parseObject(Object json);\n\n    <T> T parseObject(Object json, Class<T> clazz);\n\n    L parseArray(Object json);\n\n    <T> List<T> parseArray(Object json, Class<T> clazz);\n\n    default String format(Object obj) {\n        return toJSONString(obj, true);\n    }\n    default String toJSONString(Object obj) {\n        return toJSONString(obj, false);\n    }\n    String toJSONString(Object obj, boolean format);\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONRequest.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Map;\n\nimport static apijson.StringUtil.PATTERN_ALPHA_BIG;\n\n/**wrapper for request\n * @author Lemon\n * @see #puts\n * @see #toArray\n * @use JSONRequest<M, L> request = JSON.createJSONObject(...);\n * <br> request.puts(...);//not a must\n * <br> request.toArray(...);//not a must\n */\npublic interface JSONRequest<M extends Map<String, Object>, L extends List<Object>> extends JSONMap<M, L> {\n\n\t//default JSONRequest() {\n\t//\tsuper();\n\t//}\n\t///**\n\t// * @param object must be annotated by {@link MethodAccess}\n\t// * @see\t{@link #JSONRequest(String, Object)}\n\t// */\n\t//default JSONRequest(Object object) {\n\t//\tthis(null, object);\n\t//}\n\t///**\n\t// * @param name\n\t// * @param object\n\t// * @see {@link #puts(String, Object)}\n\t// */\n\t//default JSONRequest(String name, Object object) {\n\t//\tthis();\n\t//\tputs(name, object);\n\t//}\n\n\t//public static JSONRequest<M, L> valueOf(Object obj) {\n\t//\tJSONRequest<M, L> req = new JSONRequest() {};\n\t//\tMap<String, Object> m = JSON.parseObject(obj);\n\t//\tif (m != null && ! m.isEmpty()) {\n\t//\t\treq.map.putAll(m);\n\t//\t}\n\t//\treturn req;\n\t//}\n\n\tpublic static final String KEY_TAG = \"tag\";//只在最外层，最外层用JSONRequest\n\tpublic static final String KEY_VERSION = \"version\";//只在最外层，最外层用JSONRequest\n\tpublic static final String KEY_FORMAT = \"format\";//只在最外层，最外层用JSONRequest\n\n\t/**set \"tag\":tag in outermost layer\n\t * for write operations\n\t * @param tag\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setTag(String tag) {\n\t\treturn puts(KEY_TAG, tag);\n\t}\n\n\t/**set \"version\":version in outermost layer\n\t * for target version of request\n\t * @param version\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setVersion(Integer version) {\n\t\treturn puts(KEY_VERSION, version);\n\t}\n\n\t/**set \"format\":format in outermost layer\n\t * for format APIJSON special keys to normal keys of response\n\t * @param format\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setFormat(Boolean format) {\n\t\treturn puts(KEY_FORMAT, format);\n\t}\n\n\n\t//array object <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tpublic static final int QUERY_TABLE = 0;\n\tpublic static final int QUERY_TOTAL = 1;\n\tpublic static final int QUERY_ALL = 2;\n\n\tpublic static final String QUERY_TABLE_STRING = \"TABLE\";\n\tpublic static final String QUERY_TOTAL_STRING = \"TOTAL\";\n\tpublic static final String QUERY_ALL_STRING = \"ALL\";\n\n\tpublic static final String SUBQUERY_RANGE_ALL = \"ALL\";\n\tpublic static final String SUBQUERY_RANGE_ANY = \"ANY\";\n\n\tpublic static final String KEY_QUERY = \"query\";\n\tpublic static final String KEY_COMPAT = \"compat\";\n\tpublic static final String KEY_COUNT = \"count\";\n\tpublic static final String KEY_PAGE = \"page\";\n\tpublic static final String KEY_JOIN = \"join\";\n\tpublic static final String KEY_SUBQUERY_RANGE = \"range\";\n\tpublic static final String KEY_SUBQUERY_FROM = \"from\";\n\n\tpublic static final List<String> ARRAY_KEY_LIST = new ArrayList<>(Arrays.asList(\n        KEY_QUERY, KEY_COMPAT ,KEY_COUNT, KEY_PAGE, KEY_JOIN, KEY_SUBQUERY_RANGE, KEY_SUBQUERY_FROM\n\t));\n\n\t/**set what to query in Array layer\n\t * @param query what need to query, Table,total,ALL?\n\t * @return\n\t * @see {@link #QUERY_TABLE}\n\t * @see {@link #QUERY_TOTAL}\n\t * @see {@link #QUERY_ALL}\n\t */\n\tdefault JSONRequest<M, L> setQuery(int query) {\n\t\treturn puts(KEY_QUERY, query);\n\t}\n\n\t/**set maximum count of Tables to query in Array layer\n\t * @param count <= 0 || >= max ? max : count\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setCount(int count) {\n\t\treturn puts(KEY_COUNT, count);\n\t}\n\n\t/**set page of Tables to query in Array layer\n\t * @param page <= 0 ? 0 : page\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setPage(int page) {\n\t\treturn puts(KEY_PAGE, page);\n\t}\n\n\t/**set joins of Main Table and it's Vice Tables in Array layer\n\t * @param joins \"@/User/id@\", \"&/User/id@,>/Comment/momentId@\" ...\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setJoin(String... joins) {\n\t\treturn setJson(this, StringUtil.get(joins));\n\t}\n\n\tpublic static <M extends Map<String, Object>> M setJson(M m, String... joins) {\n\t\tm.put(KEY_JOIN, StringUtil.get(joins));\n\t\treturn m;\n\t}\n\n\t/**set range for Subquery\n\t * @param range\n\t * @return\n\t * @see {@link #SUBQUERY_RANGE_ALL}\n\t * @see {@link #SUBQUERY_RANGE_ANY}\n\t */\n\tdefault JSONRequest<M, L> setSubqueryRange(String range) {\n\t\treturn puts(KEY_SUBQUERY_RANGE, range);\n\t}\n\n\t/**set from for Subquery\n\t * @param from\n\t * @return\n\t */\n\tdefault JSONRequest<M, L> setSubqueryFrom(String from) {\n\t\treturn puts(KEY_SUBQUERY_FROM, from);\n\t}\n\n\t//array object >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t/**create a parent JSONMap named KEY_ARRAY\n\t * @param count\n\t * @param page\n\t * @return {@link #toArray(int, int)}\n\t */\n\tdefault M toArray(int count, int page) {\n\t\treturn toArray(count, page, null);\n\t}\n\n\t/**create a parent JSONMap named name+KEY_ARRAY.\n\t * @param count\n\t * @param page\n\t * @param name\n\t * @return {name+KEY_ARRAY : this}. if needs to be put, use {@link #putsAll(Map<? extends String, ? extends Object>)} instead\n\t */\n\tdefault M toArray(int count, int page, String name) {\n\t\treturn JSON.createJSONObject(StringUtil.get(name) + KEY_ARRAY, this.setCount(count).setPage(page));\n\t}\n\n\n\t@Override\n\tdefault JSONRequest<M, L> putsAll(Map<? extends String, ? extends Object> map) {\n\t\tputAll(map);\n\t\treturn this;\n\t}\n\n\t@Override\n\tdefault JSONRequest<M, L> puts(Object value) {\n\t\tput(value);\n\t\treturn this;\n\t}\n\n\t@Override\n\tdefault JSONRequest<M, L> puts(String key, Object value) {\n\t\tput(key, value);\n\t\treturn this;\n\t}\n\n\n\t/**ABCdEfg => upper ? A-B-CD-EFG : a-b-cd-efg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String recoverHyphen(@NotNull String key, Boolean upper) {\n\t\treturn recoverDivider(key, \"-\", upper);\n\t}\n\n\t/**ABCdEfg => upper ? A_B_CD_EFG : a_b_cd_efg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String recoverUnderline(@NotNull String key, Boolean upper) {\n\t\treturn recoverDivider(key, \"_\", upper);\n\t}\n\n\t/**ABCdEfg => upper ? A$B$CD$EFG : a$b$cd$efg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String recoverDollar(@NotNull String key, Boolean upper) {\n\t\treturn recoverDivider(key, \"$\", upper);\n\t}\n\n\t/**ABCdEfg => upper ? A.B.CD.EFG : a.b.cd.efg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String recoverDot(@NotNull String key, Boolean upper) {\n\t\treturn recoverDivider(key, \".\", upper);\n\t}\n\n\t/**ABCdEfg => upper ? A_B_CD_EFG : a/b/cd/efg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String recoverDivider(@NotNull String key, Boolean upper) {\n\t\treturn recoverDivider(key, \"/\", upper);\n\t}\n\n\t/**驼峰格式转为带分隔符的全大写或全小写格式\n\t * @param key\n\t * @param divider\n\t * @param upper\n\t * @return\n\t */\n\tpublic static String recoverDivider(@NotNull String key, @NotNull String divider, Boolean upper) {\n\t\tStringBuilder name = new StringBuilder();\n\t\tchar[] cs = key.toCharArray();\n\t\tint len = key.length();\n\t\tfor (int i = 0; i < len; i++) {\n\t\t\tString s = key.substring(i, i + 1);\n\t\t\tif (i > 0 && PATTERN_ALPHA_BIG.matcher(s).matches()) {\n\t\t\t\tname.append(divider);\n\t\t\t}\n\t\t\tif (upper != null) {\n\t\t\t\ts = upper ? s.toUpperCase() : s.toLowerCase();\n\t\t\t}\n\t\t\tname.append(s);\n\t\t}\n\t\treturn name.toString();\n\t}\n\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/JSONResponse.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.*;\n\n/**parser for response\n * @author Lemon\n * @see #getObject\n * @see #getList\n * @use JSONResponse response = new JSONResponse(json);\n * <br> User user = response.getObject(User.class);//not a must\n * <br> List<Comment> commenntList = response.getList(\"Comment[]\", Comment.class);//not a must\n */\npublic interface JSONResponse<M extends Map<String, Object>, L extends List<Object>> extends JSONMap<M, L> {\n\tstatic final String TAG = \"JSONResponse\";\n\n\t// 节约性能和减少 bug，除了关键词 @key ，一般都符合变量命名规范，不符合也原样返回便于调试\n\t/**格式化带 - 中横线的单词\n\t */\n\tpublic static boolean IS_FORMAT_HYPHEN = false;\n\t/**格式化带 _ 下划线的单词\n\t */\n\tpublic static boolean IS_FORMAT_UNDERLINE = false;\n\t/**格式化带 $ 美元符的单词\n\t */\n\tpublic static boolean IS_FORMAT_DOLLAR = false;\n\n\n\t//default JSONResponse() {\n\t//\tsuper();\n\t//}\n\t//default JSONResponse(Object json) {\n\t//\tthis(parseObject(json));\n\t//}\n\t//default JSONResponse(Object json, JSONParser<M, L> parser) {\n\t//\tthis(parseObject(json, parser));\n\t//}\n\t//default JSONResponse(Map<String, Object> object) {\n\t//\tsuper(format(object));\n\t//}\n\t//default JSONResponse(M object, JSONCreator<M, L> creator) {\n\t//\tsuper(format(object, creator));\n\t//}\n\n\t//状态信息，非GET请求获得的信息<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tpublic static final int CODE_SUCCESS = 200; //成功\n\tpublic static final int CODE_UNSUPPORTED_ENCODING = 400; //编码错误\n\tpublic static final int CODE_ILLEGAL_ACCESS = 401; //权限错误\n\tpublic static final int CODE_UNSUPPORTED_OPERATION = 403; //禁止操作\n\tpublic static final int CODE_NOT_FOUND = 404; //未找到\n\tpublic static final int CODE_ILLEGAL_ARGUMENT = 406; //参数错误\n\tpublic static final int CODE_NOT_LOGGED_IN = 407; //未登录\n\tpublic static final int CODE_TIME_OUT = 408; //超时\n\tpublic static final int CODE_CONFLICT = 409; //重复，已存在\n\tpublic static final int CODE_CONDITION_ERROR = 412; //条件错误，如密码错误\n\tpublic static final int CODE_UNSUPPORTED_TYPE = 415; //类型错误\n\tpublic static final int CODE_OUT_OF_RANGE = 416; //超出范围\n\tpublic static final int CODE_NULL_POINTER = 417; //对象为空\n\tpublic static final int CODE_SERVER_ERROR = 500; //服务器内部错误\n\n\n\tpublic static final String MSG_SUCCEED = \"success\"; //成功\n\tpublic static final String MSG_SERVER_ERROR = \"Internal Server Error!\"; //服务器内部错误\n\n\n\tpublic static String KEY_OK = \"ok\";\n\tpublic static String KEY_CODE = \"code\";\n\tpublic static String KEY_MSG = \"msg\";\n\tpublic static final String KEY_COUNT = \"count\";\n\tpublic static final String KEY_TOTAL = \"total\";\n\tpublic static final String KEY_INFO = \"info\"; //详细的分页信息\n\tpublic static final String KEY_FIRST = \"first\"; //是否为首页\n\tpublic static final String KEY_LAST = \"last\"; //是否为尾页\n\tpublic static final String KEY_MAX = \"max\"; //最大页码\n\tpublic static final String KEY_MORE = \"more\"; //是否有更多\n\n\t/**获取状态\n\t * @return\n\t */\n\tdefault int getCode() {\n\t\ttry {\n\t\t\treturn JSON.getIntValue(this, KEY_CODE);\n\t\t} catch (Exception e) {\n\t\t\t//empty\n\t\t}\n\t\treturn 0;\n\t}\n\t/**获取状态\n\t * @return\n\t */\n\tpublic static int getCode(Map<String, Object> reponse) {\n\t\ttry {\n\t\t\treturn JSON.getIntValue(reponse, KEY_CODE);\n\t\t} catch (Exception e) {\n\t\t\t//empty\n\t\t}\n\t\treturn 0;\n\t}\n\t/**获取状态描述\n\t * @return\n\t */\n\tdefault String getMsg() {\n\t\treturn JSON.getString(this, KEY_MSG);\n\t}\n\t/**获取状态描述\n\t * @param response\n\t * @return\n\t */\n\tpublic static String getMsg(Map<String, Object> response) {\n\t\treturn response == null ? null : JSON.getString(response, KEY_MSG);\n\t}\n\t/**获取id\n\t * @return\n\t */\n\tdefault long getId() {\n\t\ttry {\n\t\t\treturn JSON.getLongValue(this, getIdKey());\n\t\t} catch (Exception e) {\n\t\t\t//empty\n\t\t}\n\t\treturn 0;\n\t}\n\t/**获取数量\n\t * @return\n\t */\n\tdefault int getCount() {\n\t\ttry {\n\t\t\treturn JSON.getIntValue(this, KEY_COUNT);\n\t\t} catch (Exception e) {\n\t\t\t//empty\n\t\t}\n\t\treturn 0;\n\t}\n\t/**获取总数\n\t * @return\n\t */\n\tdefault int getTotal() {\n\t\ttry {\n\t\t\treturn JSON.getIntValue(this, KEY_TOTAL);\n\t\t} catch (Exception e) {\n\t\t\t//empty\n\t\t}\n\t\treturn 0;\n\t}\n\n\n\t/**是否成功\n\t * @return\n\t */\n\tdefault boolean isSuccess() {\n\t\treturn isSuccess(getCode());\n\t}\n\t/**是否成功\n\t * @param code\n\t * @return\n\t */\n\tpublic static boolean isSuccess(int code) {\n\t\treturn code == CODE_SUCCESS;\n\t}\n\t/**是否成功\n\t * @param response\n\t * @return\n\t */\n\tpublic static boolean isSuccess(JSONResponse<?, ?> response) {\n\t\treturn response != null && response.isSuccess();\n\t}\n\t/**是否成功\n\t * @param response\n\t * @return\n\t */\n\tpublic static boolean isSuccess(Map<String, Object> response) {\n\t\treturn response != null && isSuccess(JSON.getIntValue(response, KEY_CODE));\n    }\n\n\t/**校验服务端是否存在table\n\t * @return\n\t */\n\tdefault boolean isExist() {\n\t\treturn isExist(getCount());\n\t}\n\t/**校验服务端是否存在table\n\t * @param count\n\t * @return\n\t */\n\tpublic static boolean isExist(int count) {\n\t\treturn count > 0;\n\t}\n\t/**校验服务端是否存在table\n\t * @param response\n\t * @return\n\t */\n\tpublic static boolean isExist(JSONResponse<?, ?> response) {\n\t\treturn response != null && response.isExist();\n\t}\n\tpublic static boolean isExist(Map<String, Object> response) {\n\t\treturn response != null && isExist(JSON.getIntValue(response, KEY_COUNT));\n\t}\n\n\t/**获取内部的JSONResponse\n\t * @param key\n\t * @return\n\t */\n\tdefault JSONResponse<M, L> getJSONResponse(String key) {\n\t\treturn getObject(key, JSONResponse.class);\n\t}\n\n\t//cannot get javaBeanDeserizer\n\t//\t/**获取内部的JSONResponse\n\t//\t * @param response\n\t//\t * @param key\n\t//\t * @return\n\t//\t */\n\t//\tpublic static JSONResponse getJSONResponse(JSONRequest response, String key) {\n\t//\t\treturn response == null ? null : response.getObject(key, JSONResponse.class);\n\t//\t}\n\t//状态信息，非GET请求获得的信息>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t/**\n\t * key = clazz.getSimpleName()\n\t * @param clazz\n\t * @return\n\t */\n\tdefault <T> T getObject(Class<T> clazz) {\n\t\treturn getObject(clazz == null ? \"\" : clazz.getSimpleName(), clazz);\n\t}\n\t/**\n\t * @param key\n\t * @param clazz\n\t * @return\n\t */\n\tdefault <T> T getObject(String key, Class<T> clazz) {\n\t\treturn getObject(this, key, clazz);\n\t}\n\t/**\n\t * @param object\n\t * @param key\n\t * @param clazz\n\t * @return\n\t */\n\tpublic static <T> T getObject(\n\t\t\tMap<String, Object> object, String key, Class<T> clazz) {\n\t\treturn toObject(object == null ? null : JSON.get(object, formatObjectKey(key)), clazz);\n\t}\n\n\t/**\n\t * @param clazz\n\t * @return\n\t */\n\tdefault <T> T toObject(Class<T> clazz) {\n\t\treturn toObject(this, clazz);\n\t}\n\n\t/**\n\t * @param object\n\t * @param clazz\n\t * @return\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> T toObject(\n\t\t\tMap<String, Object> object, Class<T> clazz) {\n\t\treturn JSON.parseObject(object, clazz);\n\t}\n\n\n\n\t/**\n\t * arrayObject = this\n\t * @param key\n\t * @return\n\t */\n\tdefault <T> List<T> getList(String key) {\n\t\treturn JSON.getList(this, key);\n\t}\n\n\t/**\n\t * key = KEY_ARRAY\n\t * @param object\n\t * @return\n\t */\n\tpublic static <T> List<T> getList(Map<String, Object> object) {\n\t\treturn JSON.getList(object, KEY_ARRAY);\n\t}\n\t/**\n\t * @param object\n\t * @param key\n\t * @param clazz\n\t * @return\n\t */\n\tpublic static <T, M extends Map<String, Object>> List<T> getList(Map<String, Object> object, String key, Class<T> clazz) {\n\t\treturn object == null ? null : JSON.parseArray(JSON.getString(object, formatArrayKey(key)), clazz);\n\t}\n\n\t/**\n\t * key = KEY_ARRAY\n\t * @return\n\t */\n\tdefault <L extends List<Object>> L getArray() {\n\t\treturn getArray(KEY_ARRAY);\n\t}\n\t/**\n\t * @param key\n\t * @return\n\t */\n\tdefault <L extends List<Object>> L getArray(String key) {\n\t\treturn getArray(this, key);\n\t}\n\t/**\n\t * @param object\n\t * @return\n\t */\n\tpublic static <L extends List<Object>> L getArray(Map<String, Object> object) {\n\t\treturn getArray(object, KEY_ARRAY);\n\t}\n\t/**\n\t * key = KEY_ARRAY\n\t * @param object\n\t * @param key\n\t * @return\n\t */\n\tpublic static <L extends List<Object>> L getArray(Map<String, Object> object, String key) {\n\t\treturn object == null ? null : JSON.get(object, formatArrayKey(key));\n\t}\n\n\n\t//\t/**\n\t//\t * @return\n\t//\t */\n\t//\tdefault JSONRequest format() {\n\t//\t\treturn format(this);\n\t//\t}\n\t/**格式化key名称\n\t * @param object\n\t * @return\n\t */\n\tpublic static <M extends Map<String, Object>, L extends List<Object>> M format(final M object) {\n\t\t//太长查看不方便，不如debug\t Log.i(TAG, \"format  object = \\n\" + JSON.toJSONString(object));\n\t\tif (object == null || object.isEmpty()) {\n\t\t\tLog.i(TAG, \"format  object == null || object.isEmpty() >> return object;\");\n\t\t\treturn object;\n\t\t}\n\n\t\tM formatedObject = JSON.createJSONObject();\n\n\t\tSet<String> set = object.keySet();\n\t\tif (set != null) {\n\n\t\t\tObject value;\n\t\t\tfor (String key : set) {\n\t\t\t\tvalue = object.get(key);\n\n\t\t\t\tif (value instanceof List<?>) {//JSONList，遍历来format内部项\n\t\t\t\t\tformatedObject.put(formatArrayKey(key), format((L) value));\n\t\t\t\t}\n\t\t\t\telse if (value instanceof Map<?, ?>) {//JSONRequest，往下一级提取\n\t\t\t\t\tformatedObject.put(formatObjectKey(key), format((M) value));\n\t\t\t\t}\n\t\t\t\telse {//其它Object，直接填充\n\t\t\t\t\tformatedObject.put(formatOtherKey(key), value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t//太长查看不方便，不如debug\t Log.i(TAG, \"format  return formatedObject = \" + JSON.toJSONString(formatedObject));\n\t\treturn formatedObject;\n\t}\n\n\t/**格式化key名称\n\t * @param array\n\t * @return\n\t */\n\tpublic static <M extends Map<String, Object>, L extends List<Object>> L format(final L array) {\n\t\t//太长查看不方便，不如debug\t Log.i(TAG, \"format  array = \\n\" + JSON.toJSONString(array));\n\t\tif (array == null || array.isEmpty()) {\n\t\t\tLog.i(TAG, \"format  array == null || array.isEmpty() >> return array;\");\n\t\t\treturn array;\n\t\t}\n\t\tL formattedArray = JSON.createJSONArray();\n\n\t\tObject value;\n\t\tfor (int i = 0; i < array.size(); i++) {\n\t\t\tvalue = array.get(i);\n\t\t\tif (value instanceof List<?>) {//JSONList，遍历来format内部项\n\t\t\t\tformattedArray.add(format((L) value));\n\t\t\t}\n\t\t\telse if (value instanceof Map<?, ?>) {//JSONRequest，往下一级提取\n\t\t\t\tformattedArray.add(format((M) value));\n\t\t\t}\n\t\t\telse {//其它Object，直接填充\n\t\t\t\tformattedArray.add(value);\n\t\t\t}\n\t\t}\n\n\t\t//太长查看不方便，不如debug\t Log.i(TAG, \"format  return formattedArray = \" + JSON.toJSONString(formattedArray));\n\t\treturn formattedArray;\n\t}\n\n\n\t/**获取表名称\n\t * @param fullName name 或 name:alias\n\t * @return name => name; name:alias => alias\n\t */\n\tpublic static String getTableName(String fullName) {\n\t\t//key:alias  -> alias; key:alias[] -> alias[]\n\t\tint index = fullName == null ? -1 : fullName.indexOf(\":\");\n\t\treturn index < 0 ? fullName : fullName.substring(0, index);\n\t}\n\n\t/**获取变量名\n\t * @param fullName\n\t * @return {@link #formatKey(String, boolean, boolean, boolean, boolean, boolean, Boolean)} formatColon = true, formatAt = true, formatHyphen = true, firstCase = true\n\t */\n\tpublic static String getVariableName(String fullName) {\n\t\tif (JSONMap.isArrayKey(fullName)) {\n\t\t\tfullName = StringUtil.addSuffix(fullName.substring(0, fullName.length() - 2), \"list\");\n\t\t}\n\t\treturn formatKey(fullName, true, true, true, true, false, true);\n\t}\n\n\t/**格式化数组的名称 key[] => keyList; key:alias[] => aliasList; Table-column[] => tableColumnList\n\t * @param key empty ? \"list\" : key + \"List\" 且首字母小写\n\t * @return {@link #formatKey(String, boolean, boolean, boolean, boolean, boolean, Boolean)} formatColon = false, formatAt = true, formatHyphen = true, firstCase = true\n\t */\n\tpublic static String formatArrayKey(String key) {\n\t\tif (JSONMap.isArrayKey(key)) {\n\t\t\tkey = StringUtil.addSuffix(key.substring(0, key.length() - 2), \"list\");\n\t\t}\n\t\tint index = key == null ? -1 : key.indexOf(\":\");\n\t\tif (index >= 0) {\n\t\t\treturn key.substring(index + 1); //不处理自定义的\n\t\t}\n\n\t\treturn formatKey(key, false, true, true, IS_FORMAT_UNDERLINE, IS_FORMAT_DOLLAR, false); //节约性能，除了数组对象 Table-column:alias[] ，一般都符合变量命名规范\n\t}\n\n\t/**格式化对象的名称 name => name; name:alias => alias\n\t * @param key name 或 name:alias\n\t * @return {@link #formatKey(String, boolean, boolean, boolean, boolean, boolean, Boolean)} formatColon = false, formatAt = true, formatHyphen = false, firstCase = true\n\t */\n\tpublic static String formatObjectKey(String key) {\n\t\tint index = key == null ? -1 : key.indexOf(\":\");\n\t\tif (index >= 0) {\n\t\t\treturn key.substring(index + 1); // 不处理自定义的\n\t\t}\n\n\t\treturn formatKey(key, false, true, IS_FORMAT_HYPHEN, IS_FORMAT_UNDERLINE, IS_FORMAT_DOLLAR, false); //节约性能，除了表对象 Table:alias ，一般都符合变量命名规范\n\t}\n\n\t/**格式化普通值的名称 name => name; name:alias => alias\n\t * @param fullName name 或 name:alias\n\t * @return {@link #formatKey(String, boolean, boolean, boolean, boolean, boolean, Boolean)} formatColon = false, formatAt = true, formatHyphen = false, firstCase = false\n\t */\n\tpublic static String formatOtherKey(String fullName) {\n\t\treturn formatKey(fullName, false, true, IS_FORMAT_HYPHEN, IS_FORMAT_UNDERLINE, IS_FORMAT_DOLLAR\n\t\t\t\t, IS_FORMAT_HYPHEN || IS_FORMAT_UNDERLINE || IS_FORMAT_DOLLAR ? false : null);\n\t}\n\n\n\t/**格式化名称\n\t * @param fullName name 或 name:alias\n\t * @param formatAt 去除前缀 @ ， @a => a\n\t * @param formatColon 去除分隔符 : ， A:b => b\n\t * @param formatHyphen 去除分隔符 - ， A-b-cd-Efg => aBCdEfg\n\t * @param formatUnderline 去除分隔符 _ ， A_b_cd_Efg => aBCdEfg\n\t * @param formatDollar 去除分隔符 $ ， A$b$cd$Efg => aBCdEfg\n\t * @param firstCase 第一个单词首字母小写，后面的首字母大写， Ab => ab ; A-b-Cd => aBCd\n\t * @return name => name; name:alias => alias\n\t */\n\tpublic static String formatKey(String fullName, boolean formatColon, boolean formatAt, boolean formatHyphen\n\t\t\t, boolean formatUnderline, boolean formatDollar, Boolean firstCase) {\n\t\tif (fullName == null) {\n\t\t\tLog.w(TAG, \"formatKey  fullName == null >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\tif (formatColon) {\n\t\t\tfullName = formatColon(fullName);\n\t\t}\n\t\tif (formatAt) { //关键词只去掉前缀，不格式化单词，例如 @a-b 返回 a-b ，最后不会调用 setter\n\t\t\tfullName = formatAt(fullName);\n\t\t}\n\t\tif (formatHyphen && fullName.contains(\"-\")) {\n\t\t\tfullName = formatHyphen(fullName, true);\n\t\t}\n\t\tif (formatUnderline && fullName.contains(\"_\")) {\n\t\t\tfullName = formatUnderline(fullName, true);\n\t\t}\n\t\tif (formatDollar && fullName.contains(\"$\")) {\n\t\t\tfullName = formatDollar(fullName, true);\n\t\t}\n\n\t\t// 默认不格式化普通 key:value (value 不为 [], {}) 的 key\n\t\treturn firstCase == null ? fullName : StringUtil.firstCase(fullName, firstCase);\n\t}\n\n\t/**\"@key\" => \"key\"\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatAt(@NotNull String key) {\n\t\treturn key.startsWith(\"@\") ? key.substring(1) : key;\n\t}\n\n\t/**key:alias => alias\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatColon(@NotNull String key) {\n\t\tint index = key.indexOf(\":\");\n\t\treturn index < 0 ? key : key.substring(index + 1);\n\t}\n\n\t/**A-b-cd-Efg => ABCdEfg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatHyphen(@NotNull String key) {\n\t\treturn StringUtil.firstCase(formatHyphen(key, true), false);\n\t}\n\t/**A-b-cd-Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatHyphen(@NotNull String key, Boolean firstCase) {\n\t\treturn formatHyphen(key, firstCase, false);\n\t}\n\t/**A-b-cd-Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @param otherCase 非首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatHyphen(@NotNull String key, Boolean firstCase, Boolean otherCase) {\n\t\treturn formatDivider(key, \"-\", firstCase, otherCase);\n\t}\n\n\t/**A_b_cd_Efg => ABCdEfg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatUnderline(@NotNull String key) {\n\t\treturn StringUtil.firstCase(formatUnderline(key, true), false);\n\t}\n\t/**A_b_cd_Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatUnderline(@NotNull String key, Boolean firstCase) {\n\t\treturn formatUnderline(key, firstCase, false);\n\t}\n\t/**A_b_cd_Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @param otherCase 非首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatUnderline(@NotNull String key, Boolean firstCase, Boolean otherCase) {\n\t\treturn formatDivider(key, \"_\", firstCase, otherCase);\n\t}\n\n\t/**A$b$cd$Efg => ABCdEfg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatDollar(@NotNull String key) {\n\t\treturn StringUtil.firstCase(formatDollar(key, true), false);\n\t}\n\t/**A$b$cd$Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDollar(@NotNull String key, Boolean firstCase) {\n\t\treturn formatDollar(key, firstCase, false);\n\t}\n\t/**A$b$cd$Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @param otherCase 非首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDollar(@NotNull String key, Boolean firstCase, Boolean otherCase) {\n\t\treturn formatDivider(key, \"$\", firstCase, otherCase);\n\t}\n\n\t/**A.b.cd.Efg => ABCdEfg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatDot(@NotNull String key) {\n\t\treturn StringUtil.firstCase(formatDot(key, true), false);\n\t}\n\t/**A.b.cd.Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDot(@NotNull String key, Boolean firstCase) {\n\t\treturn formatDot(key, firstCase, false);\n\t}\n\t/**A.b.cd.Efg => ABCdEfg\n\t * @param key\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @param otherCase 非首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDot(@NotNull String key, Boolean firstCase, Boolean otherCase) {\n\t\treturn formatDivider(key, \".\", firstCase, otherCase);\n\t}\n\n\t/**A/b/cd/Efg => ABCdEfg\n\t * @param key\n\t * @return\n\t */\n\tpublic static String formatDivider(@NotNull String key, Boolean firstCase) {\n\t\treturn formatDivider(key, \"/\", firstCase);\n\t}\n\n\t/**去除分割符，返回驼峰格式\n\t * @param key\n\t * @param divider\n\t * @return\n\t */\n\tpublic static String formatDivider(@NotNull String key, @NotNull String divider) {\n\t\treturn StringUtil.firstCase(formatDivider(key, divider, true), false);\n\t}\n\t/**去除分割符，返回驼峰格式\n\t * @param key\n\t * @param divider\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDivider(@NotNull String key, @NotNull String divider, Boolean firstCase) {\n\t\treturn formatDivider(key, divider, firstCase, false);\n\t}\n\n\t/**去除分割符，返回驼峰格式\n\t * @param key\n\t * @param divider\n\t * @param firstCase 首字符的大小写，true-大写，false-小写，null-不处理\n\t * @param otherCase 非首字符的大小写，true-大写，false-小写，null-不处理\n\t * @return\n\t */\n\tpublic static String formatDivider(@NotNull String key, @NotNull String divider, Boolean firstCase, Boolean otherCase) {\n\t\tString[] parts = StringUtil.split(key, divider);\n\t\tStringBuilder name = new StringBuilder();\n\t\tfor (String part : parts) {\n\t\t\tif (otherCase != null) {\n\t\t\t\tpart = otherCase ? part.toUpperCase() : part.toLowerCase();\n\t\t\t}\n\t\t\tif (firstCase != null) {\n\t\t\t\tpart = StringUtil.firstCase(part, firstCase);\n\t\t\t}\n\t\t\tname.append(part);\n\t\t}\n\t\treturn name.toString();\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/Log.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.text.SimpleDateFormat;\n\n/**测试用日志\n * @modifier Lemon\n */\npublic class Log {\n\tpublic static boolean DEBUG = false;\n\n\tpublic static final String LEVEL_VERBOSE = \"VERBOSE\";\n\tpublic static final String LEVEL_INFO = \"INFO\";\n\tpublic static final String LEVEL_DEBUG = \"DEBUG\";\n\tpublic static final String LEVEL_WARN = \"WARN\";\n\tpublic static final String LEVEL_ERROR = \"ERROR\";\n\n\tpublic static String LEVEL = LEVEL_WARN;\n\n\tpublic static final String VERSION = \"8.1.5\";\n\tpublic static final String KEY_SYSTEM_INFO_DIVIDER = \"\\n---|-----APIJSON SYSTEM INFO-----|---\\n\";\n\n\tpublic static final String OS_NAME;\n\tpublic static final String OS_VERSION;\n\tpublic static final String OS_ARCH;\n\tpublic static final String JAVA_VERSION;\n\tstatic {\n\t\tOS_NAME = System.getProperty(\"os.name\");\n\t\tOS_VERSION = System.getProperty(\"os.version\");\n\t\tOS_ARCH = System.getProperty(\"os.arch\");\n\t\tJAVA_VERSION = System.getProperty(\"java.version\");\n\t}\n\n\n\t//默认的时间格式\n\tpublic static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(\"yyyy-MM-dd hh:mm:ss.SSS\");\n\n\t/**\n\t * modify date format\n\t * @param dateFormatString\n\t */\n\tpublic static void setDateFormat(String dateFormatString) {\n\t\tDATE_FORMAT = new SimpleDateFormat(dateFormatString);\n\t}\n\n\t/**\n\t * log info by level tag and msg\n\t * @param TAG\n\t * @param msg\n\t * @param level\n\t */\n\tpublic static void logInfo(String TAG, String msg, String level) {\n\t\tif (level == null || level.isEmpty()) {\n\t\t\tlevel = LEVEL;\n\t\t}\n\n\t\tif (level.equals(LEVEL_VERBOSE) || level.equals(LEVEL_INFO)) {\n\t\t\tSystem.out.println(DATE_FORMAT.format(System.currentTimeMillis()) + \": \" + TAG + \".\" + level + \": \" + msg);\n\t\t}\n\t\telse if (level.equals(LEVEL_DEBUG) || level.equals(LEVEL_ERROR) || level.equals(LEVEL_WARN)) {\n\t\t\tSystem.err.println(DATE_FORMAT.format(System.currentTimeMillis()) + \": \" + TAG + \".\" + level + \": \" + msg);\n\t\t}\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void d(String TAG, String msg) {\n\t\tif (DEBUG) {\n\t\t\tlogInfo(TAG, msg, LEVEL_DEBUG);\n\t\t}\n\t}\n\n\t/**\n\t * Forced debug\n\t * @param TAG tag\n\t * @param msg debug messages\n\t */\n\tpublic static void fd(String TAG, String msg) {\n\t\tlogInfo(TAG, msg, LEVEL_DEBUG);\n\t}\n\n\t/**\n\t * Generate separation line\n\t * @param pre prefix\n\t * @param symbol used for generating separation line\n\t * @param post postfix\n\t */\n\tpublic static void sl(String pre, char symbol, String post) {\n\t\tSystem.err.println(pre + new String(new char[48]).replace('\\u0000', symbol) + post);\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void v(String TAG, String msg) {\n\t\tif (DEBUG) {\n\t\t\tlogInfo(TAG, msg, LEVEL_VERBOSE);\n\t\t}\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void i(String TAG, String msg) {\n\t\tif (DEBUG) {\n\t\t\tlogInfo(TAG, msg, LEVEL_INFO);\n\t\t}\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void e(String TAG, String msg) {\n\t\tif (DEBUG) {\n\t\t\tlogInfo(TAG, msg, LEVEL_ERROR);\n\t\t}\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void e(String TAG, String msg, Throwable e) {\n\t\tif (DEBUG) {\n\t\t\tif (e != null) {\n\t\t\t\te.printStackTrace();\n\t\t\t}\n\t\t\tlogInfo(TAG, msg, LEVEL_ERROR);\n\t\t}\n\t}\n\n\t/**\n\t * @param TAG\n\t * @param msg\n\t */\n\tpublic static void w(String TAG, String msg) {\n\t\tif (DEBUG) {\n\t\t\tlogInfo(TAG, msg, LEVEL_WARN);\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/MethodAccess.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Inherited;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static apijson.orm.AbstractVerifier.ADMIN;\nimport static apijson.orm.AbstractVerifier.CIRCLE;\nimport static apijson.orm.AbstractVerifier.CONTACT;\nimport static apijson.orm.AbstractVerifier.LOGIN;\nimport static apijson.orm.AbstractVerifier.OWNER;\nimport static apijson.orm.AbstractVerifier.UNKNOWN;\nimport static java.lang.annotation.ElementType.TYPE;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n/**请求方法权限，只允许某些角色通过对应方法访问\n * @author Lemon\n */\n@Documented\n@Retention(RUNTIME)\n@Target(TYPE)\n@Inherited\npublic @interface MethodAccess {\n\n\t/**@see {@link RequestMethod#GET}\n\t * @return 该请求方法允许的角色 default {UNKNOWN, LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\t */\n\tString[] GET() default {UNKNOWN, LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#HEAD}\n\t * @return 该请求方法允许的角色 default {UNKNOWN, LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\t */\n\tString[] HEAD() default {UNKNOWN, LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#GETS}\n\t * @return 该请求方法允许的角色 default {LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\t */\n\tString[] GETS() default {LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#HEADS}\n\t * @return 该请求方法允许的角色 default {LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\t */\n\tString[] HEADS() default {LOGIN, CONTACT, CIRCLE, OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#POST}\n\t * @return 该请求方法允许的角色  default {LOGIN, ADMIN};\n\t */\n\tString[] POST() default {OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#PUT}\n\t * @return 该请求方法允许的角色 default {OWNER, ADMIN};\n\t */\n\tString[] PUT() default {OWNER, ADMIN};\n\n\t/**@see {@link RequestMethod#DELETE}\n\t * @return 该请求方法允许的角色 default {OWNER, ADMIN};\n\t */\n\tString[] DELETE() default {OWNER, ADMIN};\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/NotNull.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Target;\n\nimport static java.lang.annotation.ElementType.ANNOTATION_TYPE;\nimport static java.lang.annotation.ElementType.CONSTRUCTOR;\nimport static java.lang.annotation.ElementType.FIELD;\nimport static java.lang.annotation.ElementType.METHOD;\nimport static java.lang.annotation.ElementType.PARAMETER;\nimport static java.lang.annotation.RetentionPolicy.RUNTIME;\n\n\n/**非null注解\n * javax.validation.constraints.NotNull不在JDK里面，为了减少第三方库引用就在这里实现了一个替代品\n * @author Lemon\n */\n@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })\n@Retention(RUNTIME)\n@Documented\npublic @interface NotNull {\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/RequestMethod.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.util.Arrays;\nimport java.util.List;\n\n/**请求方法，对应org.springframework.web.bind.annotation.RequestMethod，多出GETS,HEADS方法\n * @author Lemon\n */\npublic enum RequestMethod {\n\n\t/**\n\t * 常规获取数据方式\n\t */\n\tGET,\n\t\n\t/**\n\t * 检查，默认是非空检查，返回数据总数\n\t */\n\tHEAD,\n\t\n\t/**Safe, Single, Simple\n\t * <br > 限制性GET，通过POST来GET数据，不显示请求内容和返回结果，并且校验请求，一般用于对安全要求比较高的请求\n\t */\n\tGETS,\n\t\n\t/**Safe, Single, Simple\n\t * <br > 限制性HEAD，通过POST来HEAD数据，不显示请求内容和返回结果，并且校验请求，一般用于对安全要求比较高的请求\n\t */\n\tHEADS,\n\t\n\t/**\n\t * 新增(或者说插入)数据\n\t */\n\tPOST,\n\t\n\t/**\n\t * 修改数据，只修改传入字段对应的值\n\t */\n\tPUT,\n\t\n\t/**\n\t * 删除数据\n\t */\n\tDELETE,\n\n\t/**\n\t * json 包含多条语句，支持增删改查、函数调用\n\t */\n\tCRUD;\n\t\n\tpublic static final RequestMethod[] ALL = new RequestMethod[]{ GET, HEAD, GETS, HEADS, POST, PUT, DELETE, CRUD };\n\tpublic static final List<String> ALL_NAME_LIST = Arrays.asList(\n\t\t\tGET.name(), HEAD.name(), GETS.name(), HEADS.name(), POST.name(), PUT.name(), DELETE.name(), CRUD.name()\n\t);\n\n\t/**是否为GET请求方法\n\t * @param method\n\t * @param containPrivate 包含私密(非明文)获取方法GETS\n\t * @return\n\t */\n\tpublic static boolean isGetMethod(RequestMethod method, boolean containPrivate) {\n\t\treturn method == null || method == GET || (containPrivate && method == GETS);\n\t}\n\t\n\t/**是否为HEAD请求方法\n\t * @param method\n\t * @param containPrivate 包含私密(非明文)获取方法HEADS\n\t * @return\n\t */\n\tpublic static boolean isHeadMethod(RequestMethod method, boolean containPrivate) {\n\t\treturn method == HEAD || (containPrivate && method == HEADS);\n\t}\n\t\n\t/**是否为查询的请求方法\n\t * @param method\n\t * @return 读操作(GET型或HEAD型) - true, 写操作(POST,PUT,DELETE) - false\n\t */\n\tpublic static boolean isQueryMethod(RequestMethod method) {\n\t\treturn isGetMethod(method, true) || isHeadMethod(method, true);\n\t}\n\n\t/**是否为更新(增删改)的请求方法\n\t * @param method\n\t * @return 读操作(GET型或HEAD型) - false, 写操作(POST,PUT,DELETE) - true\n\t */\n\tpublic static boolean isUpdateMethod(RequestMethod method) {\n\t\treturn ! isQueryMethod(method);\n\t}\n\t\n\t/**是否为开放(不限制请求的结构或内容；明文，浏览器能直接访问及查看)的请求方法\n\t * @param method\n\t * @return\n\t */\n\tpublic static boolean isPublicMethod(RequestMethod method) {\n\t\treturn method == null || method == GET || method == HEAD;\n\t}\n\n\t/**是否为私有(限制请求的结构或内容)的请求方法\n\t * @param method\n\t * @return\n\t */\n\tpublic static boolean isPrivateMethod(RequestMethod method) {\n\t\treturn ! isPublicMethod(method);\n\t}\n\n\tpublic static String getName(RequestMethod method) {\n\t\treturn method == null ? GET.name() : method.name();\n\t}\n\t\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/SQL.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\n/**SQL语句，函数名尽量和JDK中相同或类似功能的函数的名称一致\n * @author Lemon\n */\npublic class SQL {\n\n\tpublic static final String JOIN = \" JOIN \";\n\tpublic static final String ON = \" ON \";\n\tpublic static final String OR = \" OR \";\n\tpublic static final String AND = \" AND \";\n\tpublic static final String NOT = \" NOT \";\n\tpublic static final String AS = \" AS \";\n\tpublic static final String IS = \" IS \";\n\tpublic static final String NULL = \" NULL \";\n\tpublic static final String IS_NOT = \" IS NOT \";\n\tpublic static final String IS_NULL = \" IS NULL \";\n\tpublic static final String IS_NOT_NULL = \" IS NOT NULL \";\n\n\t//括号必须紧跟函数名！ count (...) 报错！\n\tpublic static final String COUNT = \"count\";\n\tpublic static final String SUM = \"sum\";\n\tpublic static final String MAX = \"max\";\n\tpublic static final String MIN = \"min\";\n\tpublic static final String AVG = \"avg\";\n\n\t/**\n\t * isNull = true \n\t * @return {@link #isNull(boolean)}\n\t */\n\tpublic static String isNull() {\n\t\treturn isNull(true);\n\t}\n\t/**\n\t * @param isNull\n\t * @return {@link #IS} + (isNull ? \"\" : {@link #NOT}) + {@link #NULL};\n\t */\n\tpublic static String isNull(boolean isNull) {\n\t\treturn IS + (isNull ? \"\" : NOT) + NULL;\n\t}\n\t/**\n\t * isNull = true\n\t * @param s\n\t * @return {@link #isNull(String, boolean)}\n\t */\n\tpublic static String isNull(String s) {\n\t\treturn isNull(s, true);\n\t}\n\t/**\n\t * @param s\n\t * @param isNull\n\t * @return s + {@link #isNull(boolean)}\n\t */\n\tpublic static String isNull(String s, boolean isNull) {\n\t\treturn s + isNull(isNull);\n\t}\n\n\t/**\n\t * isEmpty = true\n\t * @param s\n\t * @return {@link #isEmpty(String, boolean)}\n\t */\n\tpublic static String isEmpty(String s) {\n\t\treturn isEmpty(s, true);\n\t}\n\t/**\n\t * trim = false\n\t * @param s\n\t * @param isEmpty\n\t * @return {@link #isEmpty(String, boolean, boolean)}\n\t */\n\tpublic static String isEmpty(String s, boolean isEmpty) {\n\t\treturn isEmpty(s, isEmpty, false);\n\t}\n\t/**\n\t * nullable = true\n\t * @param s\n\t * @param isEmpty <=0\n\t * @param trim s = trim(s);\n\t * @return {@link #isEmpty(String, boolean, boolean, boolean)}\n\t */\n\tpublic static String isEmpty(String s, boolean isEmpty, boolean trim) {\n\t\treturn isEmpty(s, isEmpty, trim, true);\n\t}\n\t/**\n\t * @param s\n\t * @param isEmpty <=0\n\t * @param trim s = trim(s);\n\t * @param nullable isNull(s, true) + {@link #OR} +\n\t * @return {@link #lengthCompare(String, String)}\n\t */\n\tpublic static String isEmpty(String s, boolean isEmpty, boolean trim, boolean nullable) {\n\t\tif (trim) {\n\t\t\ts = trim(s);\n\t\t}\n\t\treturn (nullable ? isNull(s, true) + OR : \"\") + lengthCompare(s, (isEmpty ? \"<=\" : \">\") + \"0\");\n\t}\n\t/**\n\t * @param s 因为POWER(x,y)等函数含有不只一个key，所以需要客户端添加进去，服务端检测到条件中有'('和')'时就不转换，直接当SQL语句查询\n\t * @return {@link #length(String)} + compare\n\t */\n\tpublic static String lengthCompare(String s, String compare) {\n\t\treturn length(s) + compare;\n\t}\n\n\n\t/**\n\t * @param s 因为POWER(x,y)等函数含有不只一个key，所以需要客户端添加进去，服务端检测到条件中有'('和')'时就不转换，直接当SQL语句查询\n\t * @return \"length(\" + s + \")\"\n\t */\n\tpublic static String length(String s) {\n\t\treturn \"length(\" + s + \")\";\n\t}\n\t/**\n\t * @param s 因为POWER(x,y)等函数含有不只一个key，所以需要客户端添加进去，服务端检测到条件中有'('和')'时就不转换，直接当SQL语句查询\n\t * @return \"json_length(\" + s + \")\"\n\t */\n\tpublic static String json_length(String s) {\n\t\treturn \"json_length(\" + s + \")\";\n\t}\n\t/**\n\t * @param s 因为POWER(x,y)等函数含有不只一个key，所以需要客户端添加进去，服务端检测到条件中有'('和')'时就不转换，直接当SQL语句查询\n\t * @return \"char_length(\" + s + \")\"\n\t */\n\tpublic static String charLength(String s) {\n\t\treturn \"char_length(\" + s + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @return \"trim(\" + s + \")\"\n\t */\n\tpublic static String trim(String s) {\n\t\treturn \"trim(\" + s + \")\";\n\t}\n\t/**\n\t * @param s\n\t * @return \"ltrim(\" + s + \")\"\n\t */\n\tpublic static String trimLeft(String s) {\n\t\treturn \"ltrim(\" + s + \")\";\n\t}\n\t/**\n\t * @param s\n\t * @return \"rtrim(\" + s + \")\"\n\t */\n\tpublic static String trimRight(String s) {\n\t\treturn \"rtrim(\" + s + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @param n\n\t * @return \"left(\" + s + \",\" + n + \")\"\n\t */\n\tpublic static String left(String s, int n) {\n\t\treturn \"left(\" + s + \",\" + n + \")\";\n\t}\n\t/**\n\t * @param s\n\t * @param n\n\t * @return \"right(\" + s + \",\" + n + \")\"\n\t */\n\tpublic static String right(String s, int n) {\n\t\treturn \"right(\" + s + \",\" + n + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @param start\n\t * @param end\n\t * @return \"substring(\" + s + \",\" + start + \",\" + (end-start) + \")\"\n\t */\n\tpublic static String subString(String s, int start, int end) {\n\t\treturn \"substring(\" + s + \",\" + start + \",\" + (end-start) + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @param c\n\t * @return \"instr(\" + s + \", \" + c + \")\"\n\t */\n\tpublic static String indexOf(String s, String c) {\n\t\treturn \"instr(\" + s + \", \" + c + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @param c1\n\t * @param c2\n\t * @return \"replace(\" + s + \", \" + c1 + \", \" + c2 + \")\"\n\t */\n\tpublic static String replace(String s, String c1, String c2) {\n\t\treturn \"replace(\" + s + \", \" + c1 + \", \" + c2 + \")\";\n\t}\n\n\t/**\n\t * @param s1\n\t * @param s2\n\t * @return \"concat(\" + s1 + \", \" + s2 + \")\"\n\t */\n\tpublic static String concat(String s1, String s2) {\n\t\treturn \"concat(\" + s1 + \", \" + s2 + \")\";\n\t}\n\n\t/**\n\t * @param s1\n\t * @param s2\n\t * @return \"strcmp(\" + s1 + \", \" + s2 + \")\"\n\t */\n\tpublic static String equals(String s1, String s2) {\n\t\treturn \"strcmp(\" + s1 + \", \" + s2 + \")\";\n\t}\n\n\t/**\n\t * @param s\n\t * @return \"upper(\" + s + \")\"\n\t */\n\tpublic static String toUpperCase(String s) {\n\t\treturn \"upper(\" + s + \")\";\n\t}\n\t/**\n\t * @param s\n\t * @return \"lower(\" + s + \")\"\n\t */\n\tpublic static String toLowerCase(String s) {\n\t\treturn \"lower(\" + s + \")\";\n\t}\n\n\n\n\n\t//column and function<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**字段\n\t * @param column\n\t * @return column.isEmpty() ? \"*\" : column;\n\t */\n\tpublic static String column(String column) {\n\t\tcolumn = StringUtil.trim(column);\n\t\treturn column.isEmpty() ? \"*\" : column;\n\t}\n\t/**有别名的字段\n\t * @param column\n\t * @return {@link #count(String)} + {@link #AS};\n\t */\n\tpublic static String columnAs(String column) {\n\t\treturn count(column) + AS;\n\t}\n\n\t/**函数\n\t * @param column if (StringUtil.isEmpty(column, true) || column.contains(\",\")) -> column = null;\n\t * @return \" \" + fun + \"(\" + {@link #column(String)} + \") \";\n\t */\n\tpublic static String function(String fun, String column) {\n\t\t// 支持 fun(col1,col2..)  \n\t\t//\t\tif (StringUtil.isEmpty(column, true) || column.contains(\",\")) {\n\t\t//\t\t\tcolumn = null; //解决 count(id,name) 这种多个字段导致的SQL异常\n\t\t//\t\t}\n\t\treturn \" \" + fun + \"(\" + column(column) + \") \";\n\t}\n\t/**有别名的函数\n\t * @param column\n\t * @return {@link #function(String, String)} + {@link #AS} + fun;\n\t */\n\tpublic static String functionAs(String fun, String column) {\n\t\treturn function(fun, column) + AS + fun + \" \";\n\t}\n\n\t/**计数\n\t * column = null\n\t * @return {@link #count(String)}\n\t */\n\tpublic static String count() {\n\t\treturn count(null);\n\t}\n\t/**计数\n\t * fun = {@link #COUNT}\n\t * @param column\n\t * @return {@link #functionAs(String, String)}\n\t */\n\tpublic static String count(String column) {\n\t\treturn functionAs(COUNT, column);\n\t}\n\t/**求和\n\t * fun = {@link #SUM}\n\t * @param column\n\t * @return {@link #functionAs(String, String)}\n\t */\n\tpublic static String sum(String column) {\n\t\treturn functionAs(SUM, column);\n\t}\n\t/**最大值\n\t * fun = {@link #MAX}\n\t * @param column\n\t * @return {@link #functionAs(String, String)}\n\t */\n\tpublic static String max(String column) {\n\t\treturn functionAs(MAX, column);\n\t}\n\t/**最小值\n\t * fun = {@link #MIN}\n\t * @param column\n\t * @return {@link #functionAs(String, String)}\n\t */\n\tpublic static String min(String column) {\n\t\treturn functionAs(MIN, column);\n\t}\n\t/**平均值\n\t * fun = {@link #AVG}\n\t * @param column\n\t * @return {@link #functionAs(String, String)}\n\t */\n\tpublic static String avg(String column) {\n\t\treturn functionAs(AVG, column);\n\t}\n\n\t//column and function>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//search<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tpublic static final int SEARCH_TYPE_CONTAIN_FULL = 0;\n\tpublic static final int SEARCH_TYPE_CONTAIN_ORDER = 1;\n\tpublic static final int SEARCH_TYPE_CONTAIN_SINGLE = 2;\n\tpublic static final int SEARCH_TYPE_CONTAIN_ANY = 3;\n\tpublic static final int SEARCH_TYPE_START = 4;\n\tpublic static final int SEARCH_TYPE_END = 5;\n\tpublic static final int SEARCH_TYPE_START_SINGLE = 6;\n\tpublic static final int SEARCH_TYPE_END_SINGLE = 7;\n\tpublic static final int SEARCH_TYPE_PART_MATCH = 8;\n\t/**获取搜索值\n\t * @param s\n\t * @return\n\t */\n\tpublic static String search(String s) {\n\t\treturn search(s, SEARCH_TYPE_CONTAIN_FULL);\n\t}\n\t/**获取搜索值\n\t * @param s\n\t * @param type\n\t * @return\n\t */\n\tpublic static String search(String s, int type) {\n\t\treturn search(s, type, true);\n\t}\n\t/**获取搜索值\n\t * @param s\n\t * @param type\n\t * @param ignoreCase\n\t * @return default SEARCH_TYPE_CONTAIN_FULL\n\t */\n\tpublic static String search(String s, int type, boolean ignoreCase) {\n\t\tif (s == null) {\n\t\t\treturn null;\n\t\t}\n\t\tswitch (type) {\n\t\tcase SEARCH_TYPE_CONTAIN_SINGLE:\n\t\t\treturn \"_\" + s + \"_\";\n\t\tcase SEARCH_TYPE_CONTAIN_ORDER:\n\t\t\tchar[] cs = s.toCharArray();\n\t\t\tif (cs == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tString value = \"%\";\n\t\t\tfor (int i = 0; i < cs.length; i++) {\n\t\t\t\tvalue += cs[i] + \"%\";\n\t\t\t}\n\t\t\treturn value;\n\t\tcase SEARCH_TYPE_START:\n\t\t\treturn s + \"%\";\n\t\tcase SEARCH_TYPE_END:\n\t\t\treturn \"%\" + s;\n\t\tcase SEARCH_TYPE_START_SINGLE:\n\t\t\treturn s + \"_\";\n\t\tcase SEARCH_TYPE_END_SINGLE:\n\t\t\treturn \"_\" + s;\n\t\tcase SEARCH_TYPE_CONTAIN_ANY:\n\t\tcase SEARCH_TYPE_PART_MATCH:\n\t\t\tcs = s.toCharArray();\n\t\t\tif (cs == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tvalue = \"\";\n\t\t\tfor (int i = 0; i < cs.length; i++) {\n\t\t\t\tvalue += search(\"\" + cs[i], SEARCH_TYPE_CONTAIN_FULL, ignoreCase);\n\t\t\t}\n\t\t\treturn value;\n\t\tdefault://SEARCH_TYPE_CONTAIN_FULL\n\t\t\treturn \"%\" + s + \"%\";\n\t\t}\n\t}\n\n\t//search>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\tpublic static boolean isBooleanOrNumber(String type) {\n\t\ttype = StringUtil.toUpperCase(type, true);\n\t\treturn type.isEmpty() || (type.endsWith(\"INT\") && type.endsWith(\"POINT\") == false)\n\t\t\t\t|| type.endsWith(\"BOOLEAN\") || type.endsWith(\"ENUM\")\n\t\t\t\t|| type.endsWith(\"FLOAT\") || type.endsWith(\"DOUBLE\") || type.endsWith(\"DECIMAL\");\n\t}\n\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/StringUtil.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson;\n\nimport java.io.File;\nimport java.math.BigDecimal;\nimport java.text.DecimalFormat;\nimport java.util.Objects;\nimport java.util.regex.Pattern;\n\n/**通用字符串(String)相关类,为null时返回\"\"\n * @author Lemon\n * @use StringUtil.\n */\npublic class StringUtil {\n\tprivate static final String TAG = \"StringUtil\";\n\n\tpublic StringUtil() {\n\t}\n\n\tpublic static final String UTF_8 = \"utf-8\";\n\n\tpublic static final String EMPTY = \"无\";\n\tpublic static final String UNKNOWN = \"未知\";\n\tpublic static final String UNLIMITED = \"不限\";\n\n\tpublic static final String I = \"我\";\n\tpublic static final String YOU = \"你\";\n\tpublic static final String HE = \"他\";\n\tpublic static final String SHE = \"她\";\n\tpublic static final String IT = \"它\";\n\n\tpublic static final String MALE = \"男\";\n\tpublic static final String FEMALE = \"女\";\n\n\tpublic static final String TODO = \"未完成\";\n\tpublic static final String DONE = \"已完成\";\n\n\tpublic static final String FAIL = \"失败\";\n\tpublic static final String SUCCESS = \"成功\";\n\n\tpublic static final String SUNDAY = \"日\";\n\tpublic static final String MONDAY = \"一\";\n\tpublic static final String TUESDAY = \"二\";\n\tpublic static final String WEDNESDAY = \"三\";\n\tpublic static final String THURSDAY = \"四\";\n\tpublic static final String FRIDAY = \"五\";\n\tpublic static final String SATURDAY = \"六\";\n\n\tpublic static final String YUAN = \"元\";\n\n\n\tprivate static String current = \"\";\n\t/**获取刚传入处理后的 string\n\t * @must 上个影响 current 的方法 和 这个方法都应该在同一线程中，否则返回值可能不对\n\t * @return\n\t */\n\tpublic static String cur() {\n\t\treturn get(current);\n\t}\n\n\t/**FIXME 改用 cur\n\t * @must 上个影响currentString的方法 和 这个方法都应该在同一线程中，否则返回值可能不对\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getCurrentString() {\n\t\treturn cur();\n\t}\n\n\t//获取string,为null时返回\"\" <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**获取string,为null则返回\"\"\n\t * @param obj\n\t * @return\n\t */\n\tpublic static String get(Object obj) {\n\t\treturn obj == null ? \"\" : obj.toString();\n\t}\n\t/**获取string,为null则返回\"\"\n\t * @param s\n\t * @return\n\t */\n\tpublic static String get(String s) {\n\t\treturn s == null ? \"\" : s;\n\t}\n\t/**获取string,为null则返回\"\"\n\t * ignoreEmptyItem = false;\n\t * split = \",\"\n\t * @param arr\n\t * @return {@link #get(Object[], boolean)}\n\t */\n\tpublic static String get(Object[] arr) {\n\t\treturn get(arr, false);\n\t}\n\t/**获取string,为null则返回\"\"\n\t * split = \",\"\n\t * @param arr\n\t * @param ignoreEmptyItem\n\t * @return {@link #get(Object[], boolean)}\n\t */\n\tpublic static String get(Object[] arr, boolean ignoreEmptyItem) {\n\t\treturn get(arr, null, ignoreEmptyItem);\n\t}\n\t/**获取string,为null则返回\"\"\n\t * ignoreEmptyItem = false;\n\t * @param arr\n\t * @param split\n\t * @return {@link #get(Object[], String, boolean)}\n\t */\n\tpublic static String get(Object[] arr, String split) {\n\t\treturn get(arr, split, false);\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182\n\t/**获取string,为null则返回\"\"\n\t * @param arr -the str arr given\n\t * @param split -the token used to split\n\t * @param ignoreEmptyItem -whether to ignore empty item or not\n\t * @return {@link #get(Object[], String, boolean)}\n\t * <p>Here we replace the simple \"+\" way of concatenating with Stringbuilder 's append</p>\n\t */\n\tpublic static String get(Object[] arr, String split, boolean ignoreEmptyItem) {\n\t\tStringBuilder s = new StringBuilder(\"\");\n\t\tif (arr != null) {\n\t\t\tif (split == null) {\n\t\t\t\tsplit = \",\";\n\t\t\t}\n\t\t\tfor (int i = 0; i < arr.length; i++) {\n\t\t\t\tif (ignoreEmptyItem && isEmpty(arr[i], true)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ts.append(((i > 0 ? split : \"\") + arr[i]));\n\t\t\t}\n\t\t}\n\t\treturn get(s.toString());\n\t}\n\n\t/**FIXME 用 get 替代\n\t * @param object\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getString(Object object) {\n\t\treturn object == null ? \"\" : object.toString();\n\t}\n\t/**FIXME 用 get 替代\n\t * @param cs\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getString(CharSequence cs) {\n\t\treturn cs == null ? \"\" : cs.toString();\n\t}\n\t/**FIXME 用 get 替代\n\t * @param s\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getString(String s) {\n\t\treturn s == null ? \"\" : s;\n\t}\n\t/**FIXME 用 get 替代\n\t * ignoreEmptyItem = false;\n\t * split = \",\"\n\t * @param array\n\t * @return {@link #get(Object[], boolean)}\n\t */\n\t@Deprecated\n\tpublic static String getString(Object[] array) {\n\t\treturn get(array, false);\n\t}\n\t/**FIXME 用 get 替代\n\t * split = \",\"\n\t * @param array\n\t * @param ignoreEmptyItem\n\t * @return {@link #get(Object[], boolean)}\n\t */\n\t@Deprecated\n\tpublic static String getString(Object[] array, boolean ignoreEmptyItem) {\n\t\treturn get(array, null, ignoreEmptyItem);\n\t}\n\t/**FIXME 用 get 替代\n\t * ignoreEmptyItem = false;\n\t * @param array\n\t * @param split\n\t * @return {@link #get(Object[], String, boolean)}\n\t */\n\t@Deprecated\n\tpublic static String getString(Object[] array, String split) {\n\t\treturn get(array, split, false);\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182\n\t/**FIXME 用 get 替代\n\t * @param array -the str array given\n\t * @param split -the token used to split\n\t * @param ignoreEmptyItem -whether to ignore empty item or not\n\t * @return {@link #get(Object[], String, boolean)}\n\t * <p>Here we replace the simple \"+\" way of concatenating with Stringbuilder 's append</p>\n\t */\n\t@Deprecated\n\tpublic static String getString(Object[] array, String split, boolean ignoreEmptyItem) {\n\t\treturn get(array, split, ignoreEmptyItem);\n\t}\n\n\t//获取string,为null时返回\"\" >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t//获取去掉前后空格后的string<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**获取去掉前后空格后的string,为null则返回\"\"\n\t * @param obj\n\t * @return\n\t */\n\tpublic static String trim(Object obj) {\n\t\treturn trim(get(obj));\n\t}\n\t/**获取去掉前后空格后的string,为null则返回\"\"\n\t * @param cs\n\t * @return\n\t */\n\tpublic static String trim(CharSequence cs) {\n\t\treturn trim(get(cs));\n\t}\n\t/**获取去掉前后空格后的string,为null则返回\"\"\n\t * @param s\n\t * @return\n\t */\n\tpublic static String trim(String s) {\n\t\treturn get(s).trim();\n\t}\n\n\n\t/**FIXME 用 trim 替代\n\t * @param object\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getTrimedString(Object object) {\n\t\treturn trim(object);\n\t}\n\t/**FIXME 用 trim 替代\n\t * @param cs\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getTrimedString(CharSequence cs) {\n\t\treturn trim(cs);\n\t}\n\t/**FIXME 用 trim 替代\n\t * @param s\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getTrimedString(String s) {\n\t\treturn trim(s);\n\t}\n\n\t//获取去掉前后空格后的string>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//获取去掉所有空格后的string <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**获取去掉所有空格后的string,为null则返回\"\"\n\t * @param obj\n\t * @return\n\t */\n\tpublic static String noBlank(Object obj) {\n\t\treturn noBlank(get(obj));\n\t}\n\t/**获取去掉所有空格后的string,为null则返回\"\"\n\t * @param cs\n\t * @return\n\t */\n\tpublic static String noBlank(CharSequence cs) {\n\t\treturn noBlank(get(cs));\n\t}\n\t/**获取去掉所有空格后的string,为null则返回\"\"\n\t * @param s\n\t * @return\n\t */\n\tpublic static String noBlank(String s) {\n\t\treturn get(s).replaceAll(\"\\\\s\", \"\");\n\t}\n\n\t/**FIXME 用 noBlank 替代\n\t * @param object\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getNoBlankString(Object object) {\n\t\treturn noBlank(object);\n\t}\n\t/**FIXME 用 noBlank 替代\n\t * @param cs\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getNoBlankString(CharSequence cs) {\n\t\treturn noBlank(cs);\n\t}\n\t/**FIXME 用 noBlank 替代\n\t * @param s\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static String getNoBlankString(String s) {\n\t\treturn noBlank(s);\n\t}\n\n\t//获取去掉所有空格后的string >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//获取string的长度<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**获取string的长度,为null则返回0\n\t * @param object\n\t * @param trim\n\t * @return\n\t */\n\tpublic static int length(Object object, boolean trim) {\n\t\treturn length(get(object), trim);\n\t}\n\t/**获取string的长度,为null则返回0\n\t * @param cs\n\t * @param trim\n\t * @return\n\t */\n\tpublic static int length(CharSequence cs, boolean trim) {\n\t\treturn length(get(cs), trim);\n\t}\n\t/**获取string的长度,为null则返回0\n\t * @param s\n\t * @param trim\n\t * @return\n\t */\n\tpublic static int length(String s, boolean trim) {\n\t\ts = trim ? trim(s) : s;\n\t\treturn get(s).length();\n\t}\n\n\n\t/**FIXME 用 length 替代\n\t * @param object\n\t * @param trim\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static int getLength(Object object, boolean trim) {\n\t\treturn length(object, trim);\n\t}\n\t/**FIXME 用 length 替代\n\t * @param cs\n\t * @param trim\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static int getLength(CharSequence cs, boolean trim) {\n\t\treturn length(cs, trim);\n\t}\n\t/**FIXME 用 length 替代\n\t * @param s\n\t * @param trim\n\t * @return\n\t */\n\t@Deprecated\n\tpublic static int getLength(String s, boolean trim) {\n\t\treturn length(s, trim);\n\t}\n\n\t//获取string的长度>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//判断字符是否为空 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**判断字符是否为空 trim = true\n\t * @param obj\n\t * @return\n\t */\n\tpublic static boolean isEmpty(Object obj) {\n\t\treturn isEmpty(obj, true);\n\t}\n\t/**判断字符是否为空\n\t * @param obj\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isEmpty(Object obj, boolean trim) {\n\t\treturn isEmpty(get(obj), trim);\n\t}\n\t/**判断字符是否为空 trim = true\n\t * @param cs\n\t * @return\n\t */\n\tpublic static boolean isEmpty(CharSequence cs) {\n\t\treturn isEmpty(cs, true);\n\t}\n\t/**判断字符是否为空\n\t * @param cs\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isEmpty(CharSequence cs, boolean trim) {\n\t\treturn isEmpty(get(cs), trim);\n\t}\n\t/**判断字符是否为空 trim = true\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isEmpty(String s) {\n\t\treturn isEmpty(s, true);\n\t}\n\t/**判断字符是否为空\n\t * @param s\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isEmpty(String s, boolean trim) {\n\t\t//\t\tLog.i(TAG, \"isEmpty   s = \" + s);\n\t\tif (s == null) {\n\t\t\treturn true;\n\t\t}\n\t\tif (trim) {\n\t\t\ts = s.trim();\n\t\t}\n\t\tif (s.isEmpty()) {\n\t\t\treturn true;\n\t\t}\n\n\t\tcurrent = s;\n\n\t\treturn false;\n\t}\n\n\t//判断字符是否为空 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t//判断字符是否非空 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**判断字符是否非空 trim = true\n\t * @param obj\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(Object obj) {\n\t\treturn ! isEmpty(obj);\n\t}\n\t/**判断字符是否非空\n\t * @param obj\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(Object obj, boolean trim) {\n\t\treturn ! isEmpty(obj, trim);\n\t}\n\t/**判断字符是否非空 trim = true\n\t * @param cs\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(CharSequence cs) {\n\t\treturn ! isEmpty(cs);\n\t}\n\t/**判断字符是否非空\n\t * @param cs\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(CharSequence cs, boolean trim) {\n\t\treturn ! isEmpty(cs, trim);\n\t}\n\t/**判断字符是否非空  trim = true\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(String s) {\n\t\treturn ! isEmpty(s);\n\t}\n\t/**判断字符是否非空\n\t * @param s\n\t * @param trim\n\t * @return\n\t */\n\tpublic static boolean isNotEmpty(String s, boolean trim) {\n\t\treturn ! isEmpty(s, trim);\n\t}\n\n\t//判断字符是否非空 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//判断字符类型 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tpublic static final Pattern PATTERN_NUMBER;\n\tpublic static final Pattern PATTERN_PHONE;\n\tpublic static final Pattern PATTERN_EMAIL;\n\tpublic static final Pattern PATTERN_ID_CARD;\n\tpublic static final Pattern PATTERN_NUM_OR_ALPHA;\n\tpublic static final Pattern PATTERN_ALPHA;\n\tpublic static final Pattern PATTERN_PASSWORD; //TODO\n\tpublic static final Pattern PATTERN_NAME;\n\tpublic static final Pattern PATTERN_ALPHA_BIG;\n\tpublic static final Pattern PATTERN_ALPHA_SMALL;\n\tpublic static final Pattern PATTERN_BRANCH_URL;\n\tstatic {\n\t\tPATTERN_NUMBER = Pattern.compile(\"^[0-9]+$\");\n\t\tPATTERN_NUM_OR_ALPHA = Pattern.compile(\"^[0-9a-zA-Z_.:]+$\");\n\t\tPATTERN_ALPHA = Pattern.compile(\"^[a-zA-Z]+$\");\n\t\tPATTERN_ALPHA_BIG = Pattern.compile(\"^[A-Z]+$\");\n\t\tPATTERN_ALPHA_SMALL = Pattern.compile(\"^[a-z]+$\");\n\t\tPATTERN_NAME = Pattern.compile(\"^[0-9a-zA-Z_.:]+$\");//已用55个中英字符测试通过\n\t\t//newest phone regex expression reference https://github.com/VincentSit/ChinaMobilePhoneNumberRegex\n\t\tPATTERN_PHONE = Pattern.compile(\"^1(?:3\\\\d{3}|5[^4\\\\D]\\\\d{2}|8\\\\d{3}|7(?:[0-35-9]\\\\d{2}|4(?:0\\\\d|1[0-2]|9\\\\d))|9[0-35-9]\\\\d{2}|6[2567]\\\\d{2}|4(?:(?:10|4[01])\\\\d{3}|[68]\\\\d{4}|[579]\\\\d{2}))\\\\d{6}$\");\n\t\tPATTERN_EMAIL = Pattern.compile(\"^([a-zA-Z0-9_\\\\-\\\\.]+)@((\\\\[[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.)|(([a-zA-Z0-9\\\\-]+\\\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\\\]?)$\");\n\t\tPATTERN_ID_CARD = Pattern.compile(\"(^[1-9]\\\\d{5}(18|19|([23]\\\\d))\\\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|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)\\\\d{2}$)\");\n\t\tPATTERN_PASSWORD = Pattern.compile(\"^[0-9a-zA-Z]+$\");\n\t\tPATTERN_BRANCH_URL = Pattern.compile(\"^[0-9a-zA-Z-_/]+$\");\n\t}\n\n\t/**判断手机格式是否正确\n\t * @param phone\n\t * @return\n\t */\n\tpublic static boolean isPhone(String phone) {\n\t\tif (isNotEmpty(phone, true) == false) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = phone;\n\t\treturn PATTERN_PHONE.matcher(phone).matches();\n\t}\n\t/**判断手机格式是否正确\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isPassword(String s) {\n\t\treturn length(s, false) >= 6 && PATTERN_PASSWORD.matcher(s).matches();\n\t}\n\t/**判断是否全是数字密码\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isNumberPassword(String s) {\n\t\treturn length(s, false) == 6 && isNumber(s);\n\t}\n\t/**判断email格式是否正确\n\t * @param email\n\t * @return\n\t */\n\tpublic static boolean isEmail(String email) {\n\t\tif (isEmpty(email, true)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = email;\n\t\treturn PATTERN_EMAIL.matcher(email).matches();\n\t}\n\n\n\t/**判断是否全是验证码\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isVerify(String s) {\n\t\treturn length(s, false) >= 4 && isNumber(s);\n\t}\n\t/**判断是否全是数字\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isNumber(String s) {\n\t\tif (isEmpty(s, true)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = s;\n\t\treturn PATTERN_NUMBER.matcher(s).matches();\n\t}\n\t/**判断是否全是字母\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isAlpha(String s) {\n\t\tif (isEmpty(s, true)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = s;\n\t\treturn PATTERN_ALPHA.matcher(s).matches();\n\t}\n\t/**判断是否全是数字或字母\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isNumberOrAlpha(String s) {\n\t\treturn isNumber(s) || isAlpha(s);\n\t}\n\n\t/**判断是否全是数字或字母\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isCombineOfNumOrAlpha(String s) {\n\t\tif (isEmpty(s, true)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = s;\n\t\treturn PATTERN_NUM_OR_ALPHA.matcher(s).matches();\n\t}\n\n\t/**判断是否为代码名称，只能包含字母，数字或下划线\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isName(String s) {\n\t\tif (s == null || s.isEmpty()) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\tString first = s.substring(0, 1);\n\t\tif (\"_\".equals(first) == false && PATTERN_ALPHA.matcher(first).matches() == false) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\treturn s.length() <= 1 ? true : PATTERN_NAME.matcher(s.substring(1)).matches();\n\t}\n\t/**判断是否为首字母大写的代码名称\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isBigName(String s) {\n\t\tif (s == null || s.isEmpty() || PATTERN_ALPHA_BIG.matcher(s.substring(0, 1)).matches() == false) {\n\t\t\treturn false;\n\t\t}\n\t\treturn s.length() <= 1 ? true : PATTERN_NAME.matcher(s.substring(1)).matches();\n\t}\n\t/**判断是否为首字母小写的代码名称\n\t * @param s\n\t * @return\n\t */\n\tpublic static boolean isSmallName(String s) {\n\t\tif (s == null || s.isEmpty() || PATTERN_ALPHA_SMALL.matcher(s.substring(0, 1)).matches() == false) {\n\t\t\treturn false;\n\t\t}\n\t\treturn s.length() <= 1 ? true : PATTERN_NAME.matcher(s.substring(1)).matches();\n\t}\n\n\n\t/**判断字符类型是否是身份证号\n\t * @param number\n\t * @return\n\t */\n\tpublic static boolean isIDCard(String number) {\n\t\tif (isCombineOfNumOrAlpha(number) == false) {\n\t\t\treturn false;\n\t\t}\n\t\tnumber = get(number);\n\t\tif (number.length() == 15) {\n\t\t\tLog.i(TAG, \"isIDCard number.length() == 15 old IDCard\");\n\t\t\tcurrent = number;\n\t\t\treturn true;\n\t\t}\n\t\tif (number.length() == 18) {\n\t\t\tcurrent = number;\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic static final String HTTP = \"http\";\n\tpublic static final String URL_PREFIX = \"http://\";\n\tpublic static final String URL_PREFIXs = \"https://\";\n\t/**判断字符类型是否是网址\n\t * @param url\n\t * @return\n\t */\n\tpublic static boolean isUrl(String url) {\n\t\tif (isNotEmpty(url, true) == false) {\n\t\t\treturn false;\n\t\t} \n\t\tif (! url.startsWith(URL_PREFIX) && ! url.startsWith(URL_PREFIXs)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = url;\n\t\treturn true;\n\t}\n\n\tpublic static boolean isBranchUrl(String branchUrl) {\n\t\tif (isEmpty(branchUrl, false)) {\n\t\t\treturn false;\n\t\t}\n\t\t\n\t\treturn PATTERN_BRANCH_URL.matcher(branchUrl).matches();\n\t}\n\n\n\tpublic static final String FILE_PATH_PREFIX = \"file://\";\n\t/**判断文件路径是否存在\n\t * @param path\n\t * @return\n\t */\n\tpublic static boolean isFilePathExist(String path) {\n\t\treturn StringUtil.isFilePath(path) && new File(path).exists();\n\t}\n\n\tpublic static final String SEPARATOR = \"/\";\n\t/**判断是否为路径\n\t * @param path\n\t * @return\n\t */\n\tpublic static boolean isPath(String path) {\n\t\treturn StringUtil.isNotEmpty(path, true) && path.contains(SEPARATOR)\n\t\t\t\t&& path.contains(SEPARATOR + SEPARATOR) == false && path.endsWith(SEPARATOR) == false;\n\t}\n\n\t/**判断字符类型是否是路径\n\t * @param path\n\t * @return\n\t */\n\tpublic static boolean isFilePath(String path) {\n\t\tif (isNotEmpty(path, true) == false) {\n\t\t\treturn false;\n\t\t}\n\n\t\tif (! path.contains(\".\") || path.endsWith(\".\")) {\n\t\t\treturn false;\n\t\t}\n\n\t\tcurrent = path;\n\n\t\treturn true;\n\t}\n\n\t//判断字符类型 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//提取特殊字符<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**去掉string内所有非数字类型字符\n\t * @param object\n\t * @return\n\t */\n\tpublic static String getNumber(Object object) {\n\t\treturn getNumber(get(object));\n\t}\n\t/**去掉string内所有非数字类型字符\n\t * @param cs\n\t * @return\n\t */\n\tpublic static String getNumber(CharSequence cs) {\n\t\treturn getNumber(get(cs));\n\t}\n\t/**去掉string内所有非数字类型字符\n\t * @param s\n\t * @return\n\t */\n\tpublic static String getNumber(String s) {\n\t\treturn getNumber(s, false);\n\t}\n\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182\n\t/**去掉string内所有非数字类型字符\n\t * @param s -string passed in\n\t * @param onlyStart 中间有非数字时只获取前面的数字\n\t * @return limit String\n\t * <p>Here we replace the simple \"+\" way of concatenating with Stringbuilder 's append</p>\n\t */\n\tpublic static String getNumber(String s, boolean onlyStart) {\n\t\tif (isEmpty(s, true)) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tStringBuilder numberString = new StringBuilder(\"\");\n\t\tString single;\n\t\tfor (int i = 0; i < s.length(); i++) {\n\t\t\tsingle = s.substring(i, i + 1);\n\t\t\tif (isNumber(single)) {\n\t\t\t\tnumberString.append(single);\n\t\t\t} else {\n\t\t\t\tif (onlyStart) {\n\t\t\t\t\treturn numberString.toString();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn numberString.toString();\n\t}\n\n\t//提取特殊字符>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t//校正（自动补全等）字符串<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**获取网址，自动补全\n\t * @param url\n\t * @return\n\t */\n\tpublic static String getCorrectUrl(String url) {\n\t\tLog.i(TAG, \"getCorrectUrl : \\n\" + url);\n\t\tif (isNotEmpty(url, true) == false) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\t//\t\tif (! url.endsWith(\"/\") && ! url.endsWith(\".html\")) {\n\t\t//\t\t\turl = url + \"/\";\n\t\t//\t\t}\n\n\t\tif (isUrl(url) == false) {\n\t\t\treturn URL_PREFIX + url;\n\t\t}\n\t\treturn url;\n\t}\n\n\t/**获取去掉所有 空格 、\"-\" 、\"+86\" 后的phone\n\t * @param phone\n\t * @return\n\t */\n\tpublic static String getCorrectPhone(String phone) {\n\t\tif (isNotEmpty(phone, true) == false) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tphone = noBlank(phone);\n\t\tphone = phone.replaceAll(\"-\", \"\");\n\t\tif (phone.startsWith(\"+86\")) {\n\t\t\tphone = phone.substring(3);\n\t\t}\n\t\treturn phone;\n\t}\n\n\n\t/**获取邮箱，自动补全\n\t * @param email\n\t * @return\n\t */\n\tpublic static String getCorrectEmail(String email) {\n\t\tif (isNotEmpty(email, true) == false) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\temail = noBlank(email);\n\t\tif (isEmail(email) == false && ! email.endsWith(\".com\")) {\n\t\t\temail += \".com\";\n\t\t}\n\n\t\treturn email;\n\t}\n\n\n\tpublic static final int PRICE_FORMAT_DEFAULT = 0;\n\tpublic static final int PRICE_FORMAT_PREFIX = 1;\n\tpublic static final int PRICE_FORMAT_SUFFIX = 2;\n\tpublic static final int PRICE_FORMAT_PREFIX_WITH_BLANK = 3;\n\tpublic static final int PRICE_FORMAT_SUFFIX_WITH_BLANK = 4;\n\tpublic static final String[] PRICE_FORMATS = {\n\t\t\t\"\", \"￥\", \"元\", \"￥ \", \" 元\"\n\t};\n\n\t/**获取价格，保留两位小数\n\t * @param price\n\t * @return\n\t */\n\tpublic static String getPrice(String price) {\n\t\treturn getPrice(price, PRICE_FORMAT_DEFAULT);\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/182\n\t/**获取价格，保留两位小数\n\t * @param price -price passed in\n\t * @param formatType 添加单位（元）\n\t * @return limit String\n\t * <p>Here we replace the simple \"+\" way of concatenating with Stringbuilder 's append</p>\n\t */\n\tpublic static String getPrice(String price, int formatType) {\n\t\tif (isNotEmpty(price, true) == false) {\n\t\t\treturn getPrice(0, formatType);\n\t\t}\n\n\t\t//单独写到getCorrectPrice? <<<<<<<<<<<<<<<<<<<<<<\n\t\tString correctPrice;\n\t\tStringBuilder correctPriceBuilder = new StringBuilder(\"\");\n\t\tString s;\n\t\tfor (int i = 0; i < price.length(); i++) {\n\t\t\ts = price.substring(i, i + 1);\n\t\t\tif (\".\".equals(s) || isNumber(s)) {\n\t\t\t\tcorrectPriceBuilder.append(s);\n\t\t\t}\n\t\t}\n\t\tcorrectPrice = correctPriceBuilder.toString();\n\t\t//单独写到getCorrectPrice? >>>>>>>>>>>>>>>>>>>>>>\n\n\t\tLog.i(TAG, \"getPrice  <<<<<<<<<<<<<<<<<< correctPrice =  \" + correctPrice);\n\t\tif (correctPrice.contains(\".\")) {\n\t\t\t//\t\t\tif (correctPrice.startsWith(\".\")) {\n\t\t\t//\t\t\t\tcorrectPrice = 0 + correctPrice;\n\t\t\t//\t\t\t}\n\t\t\tif (correctPrice.endsWith(\".\")) {\n\t\t\t\tcorrectPrice = correctPrice.replaceAll(\".\", \"\");\n\t\t\t}\n\t\t}\n\n\t\tLog.i(TAG, \"getPrice correctPrice =  \" + correctPrice + \" >>>>>>>>>>>>>>>>\");\n\t\treturn isNotEmpty(correctPrice, true) ? getPrice(new BigDecimal(0 + correctPrice), formatType) : getPrice(0, formatType);\n\t}\n\t/**获取价格，保留两位小数\n\t * @param price\n\t * @return\n\t */\n\tpublic static String getPrice(BigDecimal price) {\n\t\treturn getPrice(price, PRICE_FORMAT_DEFAULT);\n\t}\n\t/**获取价格，保留两位小数\n\t * @param price\n\t * @return\n\t */\n\tpublic static String getPrice(double price) {\n\t\treturn getPrice(price, PRICE_FORMAT_DEFAULT);\n\t}\n\t/**获取价格，保留两位小数\n\t * @param price\n\t * @param formatType 添加单位（元）\n\t * @return\n\t */\n\tpublic static String getPrice(BigDecimal price, int formatType) {\n\t\treturn getPrice(price == null ? 0 : price.doubleValue(), formatType);\n\t}\n\t/**获取价格，保留两位小数\n\t * @param price\n\t * @param formatType 添加单位（元）\n\t * @return\n\t */\n\tpublic static String getPrice(double price, int formatType) {\n\t\tString s = new DecimalFormat(\"#########0.00\").format(price);\n\t\tswitch (formatType) {\n\t\tcase PRICE_FORMAT_PREFIX:\n\t\t\treturn PRICE_FORMATS[PRICE_FORMAT_PREFIX] + s;\n\t\tcase PRICE_FORMAT_SUFFIX:\n\t\t\treturn s + PRICE_FORMATS[PRICE_FORMAT_SUFFIX];\n\t\tcase PRICE_FORMAT_PREFIX_WITH_BLANK:\n\t\t\treturn PRICE_FORMATS[PRICE_FORMAT_PREFIX_WITH_BLANK] + s;\n\t\tcase PRICE_FORMAT_SUFFIX_WITH_BLANK:\n\t\t\treturn s + PRICE_FORMATS[PRICE_FORMAT_SUFFIX_WITH_BLANK];\n\t\tdefault:\n\t\t\treturn s;\n\t\t}\n\t}\n\n\tpublic static String join(String[] arr) {\n\t\treturn join(arr);\n\t}\n\t/** 数组以指定分隔s拼接\n\t * @param arr\n\t * @param s\n\t * @return\n\t */\n\tpublic static String join(String[] arr, String s) {\n\t\tif (s == null) {\n\t\t\ts = \",\";\n\t\t}\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (int i = 0; i < arr.length; i++) {\n\t\t\tsb.append(arr[i]);\n\t\t\tif (i < arr.length-1) {\n\t\t\t\tsb.append(s);\n\t\t\t}\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\t/**分割路径\n\t * @param path\n\t * @return\n\t */\n\tpublic static String[] splitPath(String path) {\n\t\tif (StringUtil.isNotEmpty(path, true) == false) {\n\t\t\treturn null;\n\t\t}\n\t\treturn isPath(path) ? split(path, SEPARATOR) : new String[] {path};\n\t}\n\t/**将s分割成String[]\n\t * @param s\n\t * @return\n\t */\n\tpublic static String[] split(String s) {\n\t\treturn split(s, null);\n\t}\n\t/**将s用split分割成String[]\n\t * trim = true;\n\t * @param s\n\t * @param split\n\t * @return\n\t */\n\tpublic static String[] split(String s, String split) {\n\t\treturn split(s, split, true);\n\t}\n\t/**将s用split分割成String[]\n\t * @param s\n\t * @param trim 去掉前后两端的split\n\t * @return\n\t */\n\tpublic static String[] split(String s, boolean trim) {\n\t\treturn split(s, null, trim);\n\t}\n\t/**将s用split分割成String[]\n\t * @param s\n\t * @param split\n\t * @param trim 去掉前后两端的split\n\t * @return\n\t */\n\tpublic static String[] split(String s, String split, boolean trim) {\n\t\ts = get(s);\n\t\tif (s.isEmpty()) {\n\t\t\treturn null;\n\t\t}\n\t\tif (isEmpty(split, false)) {\n\t\t\tsplit = \",\";\n\t\t}\n\t\tif (trim) {\n\t\t\twhile (s.startsWith(split)) {\n\t\t\t\ts = s.substring(split.length());\n\t\t\t}\n\t\t\twhile (s.endsWith(split)) {\n\t\t\t\ts = s.substring(0, s.length() - split.length());\n\t\t\t}\n\t\t}\n\t\treturn s.contains(split) ? s.split(split) : new String[]{s};\n\t}\n\n\t/**\n\t * @param key\n\t * @param suffix\n\t * @return key + suffix，第一个字母小写\n\t */\n\tpublic static String addSuffix(String key, String suffix) {\n\t\tkey = noBlank(key);\n\t\tif (key.isEmpty()) {\n\t\t\treturn firstCase(suffix);\n\t\t}\n\t\treturn firstCase(key) + firstCase(suffix, true);\n\t}\n\t/**\n\t * @param key\n\t */\n\tpublic static String firstCase(String key) {\n\t\treturn firstCase(key, false);\n\t}\n\t/**\n\t * @param key\n\t * @param upper\n\t * @return\n\t */\n\tpublic static String firstCase(String key, boolean upper) {\n\t\tkey = get(key);\n\t\tif (key.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tString first = key.substring(0, 1);\n\t\tkey = (upper ? first.toUpperCase() : first.toLowerCase()) + key.substring(1, key.length());\n\n\t\treturn key;\n\t}\n\n\t/**全部大写\n\t * @param s\n\t * @return\n\t */\n\tpublic static String toUpperCase(String s) {\n\t\treturn toUpperCase(s, false);\n\t}\n\t/**全部大写\n\t * @param s\n\t * @param trim\n\t * @return\n\t */\n\tpublic static String toUpperCase(String s, boolean trim) {\n\t\ts = trim ? trim(s) : get(s);\n\t\treturn s.toUpperCase();\n\t}\n\t/**全部小写\n\t * @param s\n\t * @return\n\t */\n\tpublic static String toLowerCase(String s) {\n\t\treturn toLowerCase(s, false);\n\t}\n\t/**全部小写\n\t * @param s\n\t * @return\n\t */\n\tpublic static String toLowerCase(String s, boolean trim) {\n\t\ts = trim ? trim(s) : get(s);\n\t\treturn s.toLowerCase();\n\t}\n\n\tpublic static String concat(String left, String right) {\n\t\treturn concat(left, right, null);\n\t}\n\tpublic static String concat(String left, String right, String split) {\n\t\treturn concat(left, right, split, true);\n\t}\n\tpublic static String concat(String left, String right, boolean trim) {\n\t\treturn concat(left, right, null, trim);\n\t}\n\tpublic static String concat(String left, String right, String split, boolean trim) {\n\t\tif (isEmpty(left, trim)) {\n\t\t\treturn right;\n\t\t}\n\t\tif (isEmpty(right, trim)) {\n\t\t\treturn left;\n\t\t}\n\n\t\tif (split == null) {\n\t\t\tsplit = \",\";\n\t\t}\n\t\treturn left + split + right;\n\t}\n\n\t//校正（自动补全等）字符串>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\tpublic static boolean equals(Object s1, Object s2) {\n\t\treturn Objects.equals(s1, s2);\n\t}\n\tpublic static boolean equalsIgnoreCase(String s1, String s2) {\n\t\tif (s1 == s2) {\n\t\t\treturn true;\n\t\t}\n\t\tif (s1 == null || s2 == null) {\n\t\t\treturn false;\n\t\t}\n\t\treturn s1.equalsIgnoreCase(s2);\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractFunctionParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.*;\nimport apijson.orm.exception.UnsupportedDataTypeException;\nimport apijson.orm.script.ScriptExecutor;\n\nimport java.lang.invoke.WrongMethodTypeException;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.math.BigDecimal;\nimport java.util.*;\n\nimport static apijson.orm.AbstractSQLConfig.PATTERN_SCHEMA;\n\n/**可远程调用的函数类\n * @author Lemon\n */\npublic abstract class AbstractFunctionParser<T, M extends Map<String, Object>, L extends List<Object>>\n\t\timplements FunctionParser<T, M, L> {\n    private static final String TAG = \"AbstractFunctionParser\";\n\n    /**是否解析参数 key 的对应的值，不用手动编码 curObj.getString(key)\n     */\n    public static boolean IS_PARSE_ARG_VALUE = false;\n\n    /**开启支持远程函数\n     */\n    public static boolean ENABLE_REMOTE_FUNCTION = true;\n    /**开启支持远程函数中的 JavaScript 脚本形式\n     */\n    public static boolean ENABLE_SCRIPT_FUNCTION = true;\n\n\t// <methodName, JSONMap>\n\t// <isContain, <arguments:\"array,key\", tag:null, methods:null>>\n    public static Map<String, ScriptExecutor<?, ? extends Map<String, Object>, ? extends List<Object>>> SCRIPT_EXECUTOR_MAP;\n\tpublic static Map<String, Map<String, Object>> FUNCTION_MAP;\n\n\tstatic {\n\t\tFUNCTION_MAP = new HashMap<>();\n\t\tSCRIPT_EXECUTOR_MAP = new HashMap<>();\n\t}\n\n\tprivate Parser<T, M, L> parser;\n\tprivate RequestMethod method;\n\tprivate String tag;\n\tprivate int version;\n\tprivate String key;\n\tprivate String parentPath;\n\tprivate String currentName;\n\tprivate M request;\n\tprivate M current;\n\n\tpublic AbstractFunctionParser() {\n\t\tthis(null, null, 0, null);\n\t}\n\n\tpublic AbstractFunctionParser(RequestMethod method, String tag, int version, @NotNull M request) {\n\t\tsetMethod(method == null ? RequestMethod.GET : method);\n\t\tsetTag(tag);\n\t\tsetVersion(version);\n\t\tsetRequest(request);\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic Parser<T, M, L> getParser() {\n\t\treturn parser;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setParser(Parser<T, M, L> parser) {\n\t\tthis.parser = parser;\n\t\treturn this;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic RequestMethod getMethod() {\n\t\treturn method == null ? RequestMethod.GET : method;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setMethod(RequestMethod method) {\n\t\tthis.method = method;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getTag() {\n\t\treturn tag;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setTag(String tag) {\n\t\tthis.tag = tag;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic int getVersion() {\n\t\treturn version;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setVersion(int version) {\n\t\tthis.version = version;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getKey() {\n\t\treturn key;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setKey(String key) {\n\t\tthis.key = key;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getParentPath() {\n\t\treturn parentPath;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setParentPath(String parentPath) {\n\t\tthis.parentPath = parentPath;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getCurrentName() {\n\t\treturn currentName;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setCurrentName(String currentName) {\n\t\tthis.currentName = currentName;\n\t\treturn this;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic M getRequest() {\n\t\treturn request;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setRequest(@NotNull M request) {\n\t\tthis.request = request;\n\t\treturn this;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic M getCurrentObject() {\n\t\treturn current;\n\t}\n\n\t@Override\n\tpublic AbstractFunctionParser<T, M, L> setCurrentObject(@NotNull M current) {\n\t\tthis.current = current;\n\t\treturn this;\n\t}\n\n\t/**根据路径取 Boolean 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Boolean getArgBool(String path) {\n\t\treturn getArgVal(path, Boolean.class);\n\t}\n\n\t/**根据路径取 Integer 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Integer getArgInt(String path) {\n\t\treturn getArgVal(path, Integer.class);\n\t}\n\n\t/**根据路径取 Long 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Long getArgLong(String path) {\n\t\treturn getArgVal(path, Long.class);\n\t}\n\n\t/**根据路径取 Float 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Float getArgFloat(String path) {\n\t\treturn getArgVal(path, Float.class);\n\t}\n\n\t/**根据路径取 Double 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Double getArgDouble(String path) {\n\t\treturn getArgVal(path, Double.class);\n\t}\n\n\t/**根据路径取 Number 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Number getArgNum(String path) {\n\t\treturn getArgVal(path, Number.class);\n\t}\n\n\t/**根据路径取 BigDecimal 值\n\t * @param path\n\t * @return\n\t */\n\tpublic BigDecimal getArgDecimal(String path) {\n\t\treturn getArgVal(path, BigDecimal.class);\n\t}\n\n\t/**根据路径取 String 值\n\t * @param path\n\t * @return\n\t */\n\tpublic String getArgStr(String path) {\n\t\tObject obj = getArgVal(path);\n\t\treturn JSON.toJSONString(obj);\n\t}\n\n\t/**根据路径取 JSONMap 值\n\t * @param path\n\t * @return\n\t */\n\tpublic Map<String, Object> getArgObj(String path) {\n\t\treturn getArgVal(path, Map.class);\n\t}\n\n\t/**根据路径取 JSONList 值\n\t * @param path\n\t * @return\n\t */\n\tpublic List<Object> getArgArr(String path) {\n\t\treturn getArgVal(path, List.class);\n\t}\n\n\t/**根据路径取 List<T> 值\n\t * @param path\n\t * @return\n\t */\n\tpublic <T extends Object> List<T> getArgList(String path) {\n\t\treturn getArgList(path, null);\n\t}\n\n\t/**根据路径取 List<T> 值\n\t * @param path\n\t * @return\n\t */\n\tpublic <T extends Object> List<T> getArgList(String path, Class<T> clazz) {\n\t\tString s = getArgStr(path);\n\t\treturn JSON.parseArray(s, clazz);\n\t}\n\n\t/**根据路径取值\n\t * @param path\n\t * @return\n\t * @param <T>\n\t */\n\tpublic <T extends Object> T getArgVal(String path) {\n\t\treturn getArgVal(path, null); // 误判概率很小 false);\n\t}\n\t/**根据路径取值\n\t * @param path\n\t * @param clazz\n\t * @return\n\t * @param <T>\n\t */\n\tpublic <T extends Object> T getArgVal(String path, Class<T> clazz) {\n\t\treturn getArgVal(getCurrentObject(), path, clazz, true);\n\t}\n\t/**根据路径取值\n\t * @param path\n\t * @param clazz\n\t * @param tryAll false-仅当前对象，true-本次请求的全局对象以及 Parser<T, M, L> 缓存值\n\t * @return\n\t * @param <T>\n\t */\n\tpublic <T extends Object> T getArgVal(@NotNull M req, String path, Class<T> clazz, boolean tryAll) {\n\t\tT val = getArgValue(req, path, clazz);\n\t\tif (tryAll == false || val != null) {\n\t\t\treturn val;\n\t\t}\n\n\t\tParser<?, ?, ?> p = getParser();\n\t\tString targetPath = AbstractParser.getValuePath(getParentPath(), path);\n\t\treturn p == null ? null : (T) p.getValueByPath(targetPath);\n\t}\n\t/**根据路径从对象 obj 中取值\n\t * @param obj\n\t * @param path\n\t * @return\n\t * @param <T>\n\t */\n\tpublic static <T extends Object> T getArgVal(Map<String, Object> obj, String path) {\n\t\treturn getArgValue(obj, path, null);\n\t}\n\n\tpublic static <T extends Object> T getArgValue(Map<String, Object> obj, String path, Class<T> clazz) {\n\t\tObject v = AbstractParser.getValue(obj, StringUtil.splitPath(path));\n\n\t\tif (clazz == null) {\n\t\t\treturn (T) v;\n\t\t}\n\n\t\t// Simple type conversion\n\t\ttry {\n\t\t\tif (v == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (clazz.isInstance(v)) {\n\t\t\t\treturn (T) v;\n\t\t\t}\n\t\t\tif (clazz == String.class) {\n\t\t\t\treturn (T) String.valueOf(v);\n\t\t\t}\n\t\t\tif (clazz == Boolean.class || clazz == boolean.class) {\n\t\t\t\treturn (T) Boolean.valueOf(String.valueOf(v));\n\t\t\t}\n\t\t\tif (clazz == Integer.class || clazz == int.class) {\n\t\t\t\treturn (T) Integer.valueOf(String.valueOf(v));\n\t\t\t}\n\t\t\tif (clazz == Long.class || clazz == long.class) {\n\t\t\t\treturn (T) Long.valueOf(String.valueOf(v));\n\t\t\t}\n\t\t\tif (clazz == Double.class || clazz == double.class) {\n\t\t\t\treturn (T) Double.valueOf(String.valueOf(v));\n\t\t\t}\n\t\t\tif (clazz == Float.class || clazz == float.class) {\n\t\t\t\treturn (T) Float.valueOf(String.valueOf(v));\n\t\t\t}\n\t\t\tif (Map.class.isAssignableFrom(clazz)) {\n\t\t\t\tif (v instanceof Map) {\n\t\t\t\t\treturn (T) v;\n\t\t\t\t}\n\t\t\t\treturn (T) JSON.parseObject(v);\n\t\t\t}\n\t\t\tif (List.class.isAssignableFrom(clazz)) {\n\t\t\t\tif (v instanceof List) {\n\t\t\t\t\treturn (T) v;\n\t\t\t\t}\n\t\t\t\treturn (T) JSON.parseArray(v);\n\t\t\t}\n\t\t\t// Fallback to string conversion\n\t\t\treturn (T) v;\n\t\t} catch (Exception e) {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\n\t/**反射调用\n\t * @param function 例如get(object,key)，参数只允许引用，不能直接传值\n\t * @param current 不作为第一个参数，就不能远程调用invoke，避免死循环\n\t * @return {@link #invoke(String, M, boolean)}\n\t */\n\t@Override\n\tpublic Object invoke(@NotNull String function, @NotNull M current) throws Exception {\n\t\treturn invoke(function, current, false);\n\t}\n\t/**反射调用\n\t * @param function 例如get(object,key)，参数只允许引用，不能直接传值\n\t * @param current 不作为第一个参数，就不能远程调用invoke，避免死循环\n\t * @param containRaw 包含原始 SQL 片段\n\t * @return {@link #invoke(AbstractFunctionParser, String, M, boolean)}\n\t */\n\t@Override\n\tpublic Object invoke(@NotNull String function, @NotNull M current, boolean containRaw) throws Exception {\n\t\tif (StringUtil.isEmpty(function, true)) {\n\t\t\tthrow new IllegalArgumentException(\"字符 \" + function + \" 不合法！\");\n\t\t}\n\n\t\treturn invoke(this, function, current, containRaw);\n\t}\n\n\t/**反射调用\n\t * @param parser\n\t * @param function 例如get(Map:map,key)，参数只允许引用，不能直接传值\n     * @param current\n     * @return {@link #invoke(AbstractFunctionParser, String, Class[], Object[])}\n\t */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> Object invoke(\n\t\t\t@NotNull AbstractFunctionParser<T, M, L> parser, @NotNull String function\n\t\t\t, @NotNull Map<String, Object> current, boolean containRaw) throws Exception {\n        if (ENABLE_REMOTE_FUNCTION == false) {\n            throw new UnsupportedOperationException(\"AbstractFunctionParser.ENABLE_REMOTE_FUNCTION\" +\n                    \" == false 时不支持远程函数！如需支持则设置 AbstractFunctionParser.ENABLE_REMOTE_FUNCTION = true ！\");\n        }\n\n\t\tFunctionBean fb = parseFunction(function, current, false, containRaw);\n\n\t\tMap<String, Object> row = FUNCTION_MAP.get(fb.getMethod()); //FIXME  fb.getSchema() + \".\" + fb.getMethod()\n\t\tif (row == null) {\n\t\t\tthrow new UnsupportedOperationException(\"不允许调用远程函数 \" + fb.getMethod() + \" !\");\n\t\t}\n\n        String language = (String) row.get(\"language\");\n        String lang = \"java\".equalsIgnoreCase(language) ? null : language;\n\n        if (ENABLE_SCRIPT_FUNCTION == false && lang != null) {\n            throw new UnsupportedOperationException(\"language = \" + language + \" 不合法！AbstractFunctionParser.ENABLE_SCRIPT_FUNCTION\" +\n                    \" == false 时不支持远程函数中的脚本形式！如需支持则设置 AbstractFunctionParser.ENABLE_SCRIPT_FUNCTION = true ！\");\n        }\n\n\t\tif (lang != null && SCRIPT_EXECUTOR_MAP.get(lang) == null) {\n\t\t\tthrow new ClassNotFoundException(\"找不到脚本语言 \" + lang + \" 对应的执行引擎！请先依赖相关库并在后端 APIJSONFunctionParser<T, M, L> 中注册！\");\n\t\t}\n\n\t\tint version = row.get(\"version\") != null ? Integer.parseInt(row.get(\"version\").toString()) : 0;\n\t\tif (parser.getVersion() < version) {\n\t\t\tthrow new UnsupportedOperationException(\"不允许 version = \" + parser.getVersion() + \" 的请求调用远程函数 \" + fb.getMethod() + \" ! 必须满足 version >= \" + version + \" !\");\n\t\t}\n\t\tString tag = (String) row.get(\"tag\");  // TODO 改为 tags，类似 methods 支持多个 tag。或者干脆不要？因为目前非开放请求全都只能后端指定\n\t\tif (tag != null && tag.equals(parser.getTag()) == false) {\n\t\t\tthrow new UnsupportedOperationException(\"不允许 tag = \" + parser.getTag() + \" 的请求调用远程函数 \" + fb.getMethod() + \" ! 必须满足 tag = \" + tag + \" !\");\n\t\t}\n\t\tString[] methods = StringUtil.split((String) row.get(\"methods\"));\n\t\tList<String> ml = methods == null || methods.length <= 0 ? null : Arrays.asList(methods);\n\t\tif (ml != null && ml.contains(parser.getMethod().toString()) == false) {\n\t\t\tthrow new UnsupportedOperationException(\"不允许 method = \" + parser.getMethod() + \" 的请求调用远程函数 \" + fb.getMethod() + \" ! 必须满足 method 在 \" + Arrays.toString(methods) + \"内 !\");\n\t\t}\n\n\t\ttry {\n            return invoke(parser, fb.getMethod(), fb.getTypes(), fb.getValues(), (String) row.get(\"returnType\"), current, SCRIPT_EXECUTOR_MAP.get(lang));\n\t\t}\n        catch (Exception e) {\n\t\t\tif (e instanceof NoSuchMethodException) {\n\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + function + \" 对应的远程函数 \" + getFunction(fb.getMethod(), fb.getKeys())\n                        + \" 不在后端 \" + parser.getClass().getName() + \" 内，也不在父类中！如果需要则先新增对应方法！\"\n\t\t\t\t\t\t+ \"\\n请检查函数名和参数数量是否与已定义的函数一致！\"\n\t\t\t\t\t\t+ \"\\n且必须为 function(key0,key1,...) 这种单函数格式！\"\n\t\t\t\t\t\t+ \"\\nfunction 必须符合 Java 函数命名，key 是用于在 curObj 内取值的键！\"\n\t\t\t\t\t\t+ \"\\n调用时不要有空格！\" + (Log.DEBUG ? e.getMessage() : \"\"));\n\t\t\t}\n\t\t\tif (e instanceof InvocationTargetException) {\n\t\t\t\tThrowable te = ((InvocationTargetException) e).getTargetException();\n\t\t\t\tif (StringUtil.isEmpty(te.getMessage(), true) == false) { //到处把函数声明throws Exception改成throws Throwable挺麻烦\n\t\t\t\t\tthrow te instanceof Exception ? (Exception) te : new Exception(te.getMessage());\n\t\t\t\t}\n\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + function + \" 对应的远程函数传参类型错误！\"\n\t\t\t\t\t\t+ \"\\n请检查 key:value 中value的类型是否满足已定义的函数 \" + getFunction(fb.getMethod(), fb.getKeys()) + \" 的要求！\"\n\t\t\t\t\t\t+ (Log.DEBUG ? e.getMessage() : \"\"));\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\n\t}\n\n\t/**反射调用\n     * @param parser\n     * @param methodName\n     * @param parameterTypes\n     * @param args\n     * @return {@link #invoke(AbstractFunctionParser, String, Class[], Object[])}\n     * @throws Exception\n     */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> Object invoke(\n\t\t\t@NotNull AbstractFunctionParser<T, M, L> parser, @NotNull String methodName\n            , @NotNull Class<?>[] parameterTypes, @NotNull Object[] args) throws Exception {\n        return invoke(parser, methodName, parameterTypes, args, null, null, null);\n    }\n    /**反射调用\n     * @param parser\n     * @param methodName\n     * @param parameterTypes\n     * @param args\n     * @param returnType\n     * @param current\n     * @param scriptExecutor\n     * @return\n     * @throws Exception\n     */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> Object invoke(\n\t\t\t@NotNull AbstractFunctionParser<T, M, L> parser, @NotNull String methodName\n            , @NotNull Class<?>[] parameterTypes, @NotNull Object[] args, String returnType\n            , Map<String, Object> current, ScriptExecutor scriptExecutor) throws Exception {\n        if (scriptExecutor != null) {\n            return invokeScript(parser, methodName, parameterTypes, args, returnType, current, scriptExecutor);\n        }\n\n\t\tClass<? extends AbstractFunctionParser> cls = parser.getClass();\n        Method m = cls.getMethod(methodName, parameterTypes); // 不用判空，拿不到就会抛异常\n\n        if (Log.DEBUG) {\n            String rt = Log.DEBUG && m.getReturnType() != null ? m.getReturnType().getSimpleName() : null;\n\n            if (\"void\".equals(rt)) {\n                rt = null;\n            }\n            if (\"void\".equals(returnType)) {\n                returnType = null;\n            }\n\n            if (rt != returnType && (rt == null || rt.equals(returnType) == false)) {\n                throw new WrongMethodTypeException(\"远程函数 \" + methodName + \" 的实际返回值类型 \" + rt + \" 与 Function 表中的配置的 \" + returnType + \" 不匹配！\");\n            }\n        }\n\n        return m.invoke(parser, args);\n\t}\n\n    /**Java 调用 JavaScript 函数\n     * @param parser\n     * @param methodName\n     * @param parameterTypes\n     * @param args\n     * @param returnType\n     * @param current\n     * @return\n     * @throws Exception\n     */\n\t@SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    public static <T, M extends Map<String, Object>, L extends List<Object>> Object invokeScript(\n\t\t\t@NotNull AbstractFunctionParser<T, M, L> parser, @NotNull String methodName\n            , @NotNull Class<?>[] parameterTypes, @NotNull Object[] args, String returnType\n\t\t\t, Map<String, Object> current, ScriptExecutor scriptExecutor) throws Exception {\n    \tObject result = scriptExecutor.execute(parser, current, methodName, args);\n        if (Log.DEBUG && result != null) {\n            Class<?> rt = result.getClass(); // 作为远程函数的 js 类型应该只有 JSON 的几种类型\n            String fullReturnType = (StringUtil.isSmallName(returnType)\n                    ? returnType : (returnType.startsWith(\"JSON\") ? \"com.alibaba.fastjson.\" : \"java.lang.\") + returnType);\n\n            if ((rt == null && returnType != null) || (rt != null && returnType == null)) {\n                throw new WrongMethodTypeException(\"远程函数 \" + methodName + \" 的实际返回值类型 \"\n                        + (rt == null ? null : rt.getName()) + \" 与 Function 表中的配置的 \" + fullReturnType + \" 不匹配！\");\n            }\n\n            Class<?> cls;\n            try {\n                cls = Class.forName(fullReturnType);\n            }\n            catch (Exception e) {\n                throw new WrongMethodTypeException(\"远程函数 \" + methodName + \" 在 Function 表中的配置的类型 \"\n                        + returnType + \" 对应的 \" + fullReturnType + \" 错误！在 Java 中 Class.forName 找不到这个类型！\");\n            }\n\n            if (cls.isAssignableFrom(rt) == false) {\n                throw new WrongMethodTypeException(\"远程函数 \" + methodName + \" 的实际返回值类型 \"\n                        + (rt == null ? null : rt.getName()) + \" 与 Function 表中的配置的 \"\n                        + returnType + \" 对应的 \" + fullReturnType + \" 不匹配！\");\n            }\n        }\n\n        Log.d(TAG, \"invokeScript \" + methodName + \"(..) >>  result = \" + result);\n        return result;\n    }\n\n\n    /**解析函数\n     * @param function\n     * @param request\n     * @param isSQLFunction\n     * @return\n     * @throws Exception\n     */\n\t@NotNull\n\tpublic static FunctionBean parseFunction(@NotNull String function, @NotNull Map<String, Object> request, boolean isSQLFunction) throws Exception {\n        return parseFunction(function, request, isSQLFunction, false);\n    }\n    /**解析函数，自动解析的值类型只支持 Boolean, Number, String, Map, List\n     * @param function\n     * @param request\n     * @param isSQLFunction\n     * @param containRaw\n     * @return\n     * @throws Exception\n     */\n\tpublic static FunctionBean parseFunction(@NotNull String function, @NotNull Map<String, Object> request, boolean isSQLFunction, boolean containRaw) throws Exception {\n\n\t\tint start = function.indexOf(\"(\");\n\t\tint end = function.lastIndexOf(\")\");\n\t\tString method = (start <= 0 || end != function.length() - 1) ? null : function.substring(0, start);\n\n        int dotInd = method == null ? -1 : method.indexOf(\".\");\n        String schema = dotInd < 0 ? null : method.substring(0, dotInd);\n        method = dotInd < 0 ? method : method.substring(dotInd + 1);\n\n        if (StringUtil.isName(method) == false) {\n\t\t\tthrow new IllegalArgumentException(\"字符 \" + method + \" 不合法！函数的名称 function 不能为空且必须符合方法命名规范！\"\n\t\t\t\t\t+ \"总体必须为 function(key0,key1,...) 这种单函数格式！\"\n\t\t\t\t\t+ \"\\nfunction必须符合 \" + (isSQLFunction ? \"SQL 函数/SQL 存储过程\" : \"Java 函数\") + \" 命名，key 是用于在 request 内取值的键！\");\n\t\t}\n        if (isSQLFunction != true && schema != null) { // StringUtil.isNotEmpty(schema, false)) {\n            throw new IllegalArgumentException(\"字符 \" + schema + \" 不合法！远程函数不允许指定类名！\"\n                    + \"且必须为 function(key0,key1,...) 这种单函数格式！\"\n                    + \"\\nfunction必须符合 \" + (isSQLFunction ? \"SQL 函数/SQL 存储过程\" : \"Java 函数\") + \" 命名，key 是用于在 request 内取值的键！\");\n        }\n        if (schema != null) { // StringUtil.isName(schema) == false) {\n\t\t\tschema = extractSchema(schema, null);\n        }\n\n\t\tString[] keys = StringUtil.split(function.substring(start + 1, end));\n\t\tint length = keys == null ? 0 : keys.length;\n\n\t\tClass<?>[] types;\n\t\tObject[] values;\n\n\t\tif (isSQLFunction || IS_PARSE_ARG_VALUE) {\n\t\t\ttypes = new Class<?>[length];\n\t\t\tvalues = new Object[length];\n\n\t\t\t//碰到null就挂了！！！Number还得各种转换不灵活！不如直接传request和对应的key到函数里，函数内实现时自己 getLongValue,getJSONObject ...\n\t\t\tObject v;\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\tv = values[i] = getArgValue(request, keys[i], containRaw); // request.get(keys[i]);\n\t\t\t\tif (v == null) {\n\t\t\t\t\ttypes[i] = Object.class;\n\t\t\t\t\tvalues[i] = null;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (v instanceof Boolean) {\n\t\t\t\t\ttypes[i] = Boolean.class; //只支持JSON的几种类型\n\t\t\t\t} // 怎么都有 bug，如果是引用的值，很多情况下无法指定  //  用 1L 指定为 Long ？ 其它的默认按长度分配为 Integer 或 Long？\n\t\t\t\t//else if (v instanceof Long || v instanceof Integer || v instanceof Short) {\n\t\t\t\t//\ttypes[i] = Long.class;\n\t\t\t\t//}\n\t\t\t\telse if (v instanceof Number) {\n\t\t\t\t\ttypes[i] = Number.class;\n\t\t\t\t}\n\t\t\t\telse if (v instanceof String) {\n\t\t\t\t\ttypes[i] = String.class;\n\t\t\t\t}\n\t\t\t\telse if (v instanceof Map) { // 泛型兼容？ // JSONMap\n\t\t\t\t\ttypes[i] = Map.class;\n\t\t\t\t\t//性能比较差\n                    //values[i] = TypeUtils.cast(v, Map.class, ParserConfig.getGlobalInstance());\n\t\t\t\t}\n\t\t\t\telse if (v instanceof Collection) { // 泛型兼容？ // JSONList\n\t\t\t\t\ttypes[i] = List.class;\n\t\t\t\t\t//性能比较差\n\t\t\t\t\tList list = new ArrayList<>((Collection) v);\n                    values[i] = list; // TypeUtils.cast(v, List.class, ParserConfig.getGlobalInstance());\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new UnsupportedDataTypeException(keys[i] + \":value 中value不合法！远程函数 key():\"\n                            + function + \" 中的 arg 对应的值类型只能是 [Boolean, Number, String, JSONMap, JSONList] 中的一种！\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tClass<? extends Map> cls = JSON.createJSONObject().getClass();\n\t\t\ttypes = new Class<?>[length + 1];\n\t\t\t//types[0] = Object.class; // 泛型擦除 JSON.JSON_OBJECT_CLASS;\n\t\t\ttypes[0] = cls;\n\n\t\t\tvalues = new Object[length + 1];\n\t\t\tvalues[0] = request;\n\n\t\t\tfor (int i = 0; i < length; i++) {\n\t\t\t\ttypes[i + 1] = String.class;\n\t\t\t\tvalues[i + 1] = keys[i];\n\t\t\t}\n\t\t}\n\n\t\tFunctionBean fb = new FunctionBean();\n\t\tfb.setFunction(function);\n\t\tfb.setSchema(schema);\n\t\tfb.setMethod(method);\n\t\tfb.setKeys(keys);\n\t\tfb.setTypes(types);\n\t\tfb.setValues(values);\n\n\t\treturn fb;\n\t}\n\n\tpublic static void verifySchema(String sch, String table) {\n\t\textractSchema(sch, table);\n\t}\n\n\tpublic static String extractSchema(String sch, String table) {\n\t\tif (StringUtil.isEmpty(sch)) {\n\t\t\treturn sch;\n\t\t}\n\n\t\tif (table == null) {\n\t\t\ttable = \"Table\";\n\t\t}\n\n\t\tint ind = sch.indexOf(\"`\");\n\t\tif (ind > 0) {\n\t\t\tthrow new IllegalArgumentException(table + \": { @key(): value } 对应存储过程 value 中字符 \"\n\t\t\t\t\t+ sch + \" 不合法！`schema` 当有 ` 包裹时一定是首尾各一个，不能多也不能少！\");\n\t\t}\n\n\t\tif (ind == 0) {\n\t\t\tsch = sch.substring(1);\n\t\t\tif (sch.indexOf(\"`\") != sch.length() - 1) {\n\t\t\t\tthrow new IllegalArgumentException(table + \": { @key(): value } 对应存储过程 value 中字符 `\"\n\t\t\t\t\t\t+ sch + \" 不合法！`schema` 当有 ` 包裹时一定是首尾各一个，不能多也不能少！\");\n\t\t\t}\n\n\t\t\tsch = sch.substring(0, sch.length() - 1);\n\t\t}\n\n\t\tif (PATTERN_SCHEMA.matcher(sch).matches() == false || sch.contains(\"--\")) {\n\t\t\tthrow new IllegalArgumentException(table + \": { @key(): value } 对应存储过程 value 中字符 \"\n\t\t\t\t\t+ sch + \" 不合法！schema.function(arg) 中 schema 必须符合 数据库名/模式名 的命名规则！\"\n\t\t\t\t\t+ \"一般只能传英文字母、数字、下划线！不允许 -- 等可能导致 SQL 注入的符号！\");\n\t\t}\n\n\t\treturn sch;\n\t}\n\n\n\t/**\n\t * @param method\n\t * @param keys\n\t * @return\n\t */\n\tpublic static String getFunction(String method, String[] keys) {\n\t\tString f = method + \"(JSONMap request\";\n\n\t\tif (keys != null) {\n\t\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\t\tf += (\", String \" + keys[i]);\n\t\t\t}\n\t\t}\n\n\t\tf += \")\";\n\n\t\treturn f;\n\t}\n\n    public static <T> T getArgValue(@NotNull Map<String, Object> current, String keyOrValue) {\n        return getArgValue(current, keyOrValue, false);\n    }\n    public static <T> T getArgValue(@NotNull Map<String, Object> current, String keyOrValue, boolean containRaw) {\n        if (keyOrValue == null) {\n            return null;\n        }\n\n\n        if (keyOrValue.endsWith(\"`\") && keyOrValue.substring(1).indexOf(\"`\") == keyOrValue.length() - 2) {\n            return (T) current.get(keyOrValue.substring(1, keyOrValue.length() - 1));\n        }\n\n        if (keyOrValue.endsWith(\"'\") && keyOrValue.substring(1).indexOf(\"'\") == keyOrValue.length() - 2) {\n            return (T) keyOrValue.substring(1, keyOrValue.length() - 1);\n        }\n\n        // 传参加上 @raw:\"key()\" 避免意外情况\n        Object val = containRaw ? AbstractSQLConfig.RAW_MAP.get(keyOrValue) : null;\n        if (val != null) {\n            return (T) (\"\".equals(val) ? keyOrValue : val);\n        }\n\n        if (StringUtil.isName(keyOrValue.startsWith(\"@\") ? keyOrValue.substring(1) : keyOrValue)) {\n            return (T) current.get(keyOrValue);\n        }\n\n        if (\"true\".equals(keyOrValue)) {\n            return (T) Boolean.TRUE;\n        }\n        if (\"false\".equals(keyOrValue)) {\n            return (T) Boolean.FALSE;\n        }\n\n        // 性能更好，但居然非法格式也不报错\n        //try {\n        //    val = Boolean.valueOf(keyOrValue); // parseJSON(keyOrValue);\n        //    return (T) val;\n        //}\n        //catch (Throwable e) {\n        //    Log.d(TAG, \"getArgValue  try {\\n\" +\n        //            \"            val = Boolean.valueOf(keyOrValue);\" +\n        //            \"} catch (Throwable e) = \" + e.getMessage());\n        //}\n\n        try {\n            val = Double.valueOf(keyOrValue); // parseJSON(keyOrValue);\n            return (T) val;\n        }\n        catch (Throwable e) {\n            Log.d(TAG, \"getArgValue  try {\\n\" +\n                    \"            val = Double.valueOf(keyOrValue);\" +\n                    \"} catch (Throwable e) = \" + e.getMessage());\n        }\n\n        return (T) current.get(keyOrValue);\n    }\n\n\tpublic static class FunctionBean {\n\t\tprivate String function;\n\t\tprivate String schema;\n\t\tprivate String method;\n\t\tprivate String[] keys;\n\t\tprivate Class<?>[] types;\n\t\tprivate Object[] values;\n\n\t\tpublic String getFunction() {\n\t\t\treturn function;\n\t\t}\n\t\tpublic void setFunction(String function) {\n\t\t\tthis.function = function;\n\t\t}\n\n        public String getSchema() {\n            return schema;\n        }\n        public void setSchema(String schema) {\n            this.schema = schema;\n        }\n\n        public String getMethod() {\n\t\t\treturn method;\n\t\t}\n\t\tpublic void setMethod(String method) {\n\t\t\tthis.method = method;\n\t\t}\n\n\t\tpublic String[] getKeys() {\n\t\t\treturn keys;\n\t\t}\n\t\tpublic void setKeys(String[] keys) {\n\t\t\tthis.keys = keys;\n\t\t}\n\n\t\tpublic Class<?>[] getTypes() {\n\t\t\treturn types;\n\t\t}\n\t\tpublic void setTypes(Class<?>[] types) {\n\t\t\tthis.types = types;\n\t\t}\n\n\t\tpublic Object[] getValues() {\n\t\t\treturn values;\n\t\t}\n\t\tpublic void setValues(Object[] values) {\n\t\t\tthis.values = values;\n\t\t}\n\n\n\t\t/**\n\t\t * @param useValue\n\t\t * @return\n\t\t */\n\t\tpublic String toFunctionCallString(boolean useValue) {\n\t\t\treturn toFunctionCallString(useValue, null);\n\t\t}\n\t\t/**\n\t\t * @param useValue\n\t\t * @param quote\n\t\t * @return\n\t\t */\n\t\tpublic String toFunctionCallString(boolean useValue, String quote) {\n            //String sch = getSchema();\n\t\t\t//String s = (StringUtil.isEmpty(sch) ? \"\" : sch + \".\") + getMethod() + \"(\";\n\t\t\tString s = getMethod() + \"(\";\n\n\t\t\tObject[] args = useValue ? getValues() : getKeys();\n\t\t\tif (args != null && args.length > 0) {\n\t\t\t\tif (quote == null) {\n\t\t\t\t\tquote = \"'\";\n\t\t\t\t}\n\n\t\t\t\tObject arg;\n\t\t\t\tfor (int i = 0; i < args.length; i++) {\n\t\t\t\t\targ = args[i];\n\t\t\t\t\ts += (i <= 0 ? \"\" : \",\") + (arg instanceof Boolean || arg instanceof Number ? arg : quote + arg + quote);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn s + \")\";\n\t\t}\n\n\t}\n\n\t/**\n\t * 获取JSON对象\n\t * @param <V>  TODO\n\t * @param req\n\t * @param key\n\t * @param clazz\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic <V> V getArgVal(@NotNull M req, String key, Class<V> clazz) throws Exception {\n\t\t// Convert to JSONMap for backward compatibility, replace with proper implementation later\n\t\treturn getArgVal(req, key, clazz, false);\n\t}\n\n\t/**\n\t * 获取参数值\n\t * @param key\n\t * @param clazz 如果有clazz就返回对应的类型，否则返回原始类型\n\t * @param defaultValue\n\t * @return\n\t * @throws Exception \n\t */\n\tpublic <V> V getArgVal(String key, Class<V> clazz, boolean defaultValue) throws Exception {\n\t\tObject obj = parser != null && JSONMap.isArrayKey(key) ? AbstractParser.getValue(request, key.split(\"\\\\,\")) : request.get(key);\n\t\t\n\t\tif (clazz == null) {\n\t\t\treturn (V) obj;\n\t\t}\n\t\t\n\t\t// Replace TypeUtils with appropriate casting method\n\t\ttry {\n\t\t\tif (obj == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (clazz.isInstance(obj)) {\n\t\t\t\treturn (V) obj;\n\t\t\t}\n\t\t\tif (clazz == String.class) {\n\t\t\t\treturn (V) String.valueOf(obj);\n\t\t\t}\n\t\t\tif (clazz == Boolean.class || clazz == boolean.class) {\n\t\t\t\treturn (V) Boolean.valueOf(String.valueOf(obj));\n\t\t\t}\n\t\t\tif (clazz == Integer.class || clazz == int.class) {\n\t\t\t\treturn (V) Integer.valueOf(String.valueOf(obj));\n\t\t\t}\n\t\t\tif (clazz == Long.class || clazz == long.class) {\n\t\t\t\treturn (V) Long.valueOf(String.valueOf(obj));\n\t\t\t}\n\t\t\tif (clazz == Double.class || clazz == double.class) {\n\t\t\t\treturn (V) Double.valueOf(String.valueOf(obj));\n\t\t\t}\n\t\t\tif (clazz == Float.class || clazz == float.class) {\n\t\t\t\treturn (V) Float.valueOf(String.valueOf(obj));\n\t\t\t}\n\t\t\tif (Map.class.isAssignableFrom(clazz)) {\n\t\t\t\tif (obj instanceof Map) {\n\t\t\t\t\treturn (V) obj;\n\t\t\t\t}\n\t\t\t\treturn (V) JSON.parseObject(obj);\n\t\t\t}\n\t\t\tif (List.class.isAssignableFrom(clazz)) {\n\t\t\t\tif (obj instanceof List) {\n\t\t\t\t\treturn (V) obj;\n\t\t\t\t}\n\t\t\t\treturn (V) JSON.parseArray(obj);\n\t\t\t}\n\t\t\t// Fallback to string conversion\n\t\t\treturn (V) obj;\n\t\t} catch (Exception e) {\n\t\t\tif (defaultValue) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tthrow e;\n\t\t}\n\t}\n\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.*;\nimport apijson.orm.AbstractFunctionParser.FunctionBean;\nimport apijson.orm.exception.ConflictException;\nimport apijson.orm.exception.CommonException;\nimport apijson.orm.exception.NotExistException;\nimport apijson.orm.exception.UnsupportedDataTypeException;\n\nimport java.rmi.ServerException;\nimport java.util.*;\nimport java.util.Map.Entry;\n\nimport static apijson.JSON.*;\nimport static apijson.JSONMap.KEY_COMBINE;\nimport static apijson.JSONMap.KEY_DROP;\nimport static apijson.JSONMap.KEY_TRY;\nimport static apijson.JSONRequest.*;\nimport static apijson.RequestMethod.POST;\nimport static apijson.RequestMethod.PUT;\nimport static apijson.orm.SQLConfig.TYPE_ITEM;\nimport static apijson.RequestMethod.GET;\n\n/**简化Parser，getObject和getArray(getArrayConfig)都能用\n * @author Lemon\n */\npublic abstract class AbstractObjectParser<T, M extends Map<String, Object>, L extends List<Object>>\n\t\timplements ObjectParser<T, M, L> {\n\tprivate static final String TAG = \"AbstractObjectParser\";\n\n\t@NotNull\n\tprotected AbstractParser<T, M, L> parser;\n\t@Override\n\tpublic AbstractParser<T, M, L> getParser() {\n\t\treturn parser;\n\t}\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setParser(Parser<T, M, L> parser) {\n\t\tthis.parser = (AbstractParser<T, M, L>) parser;\n\t\treturn this;\n\t}\n\n\tprotected M request;//不用final是为了recycle\n\tprotected String parentPath;//不用final是为了recycle\n\tprotected SQLConfig<T, M, L> arrayConfig;//不用final是为了recycle\n\tprotected boolean isSubquery;\n\n\tprotected final int type;\n\tprotected final String arrayTable;\n\tprotected final List<Join<T, M, L>> joinList;\n\tprotected final boolean isTable;\n\tprotected final boolean isArrayMainTable;\n\n\tprotected final boolean tri;\n\t/**\n\t * TODO Parser内要不因为 非 TYPE_ITEM_CHILD_0 的Table 为空导致后续中断。\n\t */\n\tprotected final boolean drop;\n\tprivate List<String> stringKeyList;\n\tprivate List<String> trimKeyList;\n\n\t/**for single object\n\t */\n\tpublic AbstractObjectParser(@NotNull M request, String parentPath, SQLConfig<T, M, L> arrayConfig\n\t\t\t, boolean isSubquery, boolean isTable, boolean isArrayMainTable) throws Exception {\n\t\tif (request == null) {\n\t\t\tthrow new IllegalArgumentException(TAG + \".ObjectParser<T, M, L>  request == null!!!\");\n\t\t}\n\t\tthis.request = request;\n\t\tthis.parentPath = parentPath;\n\n\t\tthis.arrayConfig = arrayConfig;\n\t\tthis.isSubquery = isSubquery;\n\n\t\tthis.type = arrayConfig == null ? 0 : arrayConfig.getType();\n\t\tthis.arrayTable = arrayConfig == null ? null : arrayConfig.getTable();\n\t\tthis.joinList = arrayConfig == null ? null : arrayConfig.getJoinList();\n\n\t\tthis.isTable = isTable; // apijson.JSONMap.isTableKey(table);\n\t\tthis.isArrayMainTable = isArrayMainTable; // isSubquery == false && this.isTable && this.type == SQLConfig.TYPE_ITEM_CHILD_0 && RequestMethod.isGetMethod(method, true);\n\t\t//\t\tthis.isReuse = isReuse; // isArrayMainTable && arrayConfig != null && arrayConfig.getPosition() > 0;\n\n\t\tthis.objectCount = 0;\n\t\tthis.arrayCount = 0;\n\n\t\tboolean isEmpty = request.isEmpty();//empty有效 User:{}\n\t\tif (isEmpty) {\n\t\t\tthis.tri = false;\n\t\t\tthis.drop = false;\n\t\t}\n\t\telse {\n\t\t\tthis.tri = getBooleanValue(request, KEY_TRY);\n\t\t\tthis.drop = getBooleanValue(request, KEY_DROP);\n\n\t\t\trequest.remove(KEY_TRY);\n\t\t\trequest.remove(KEY_DROP);\n\t\t}\n\n        if (isTable) {\n            String raw = getString(request, JSONMap.KEY_RAW);\n            String[] rks = StringUtil.split(raw);\n            rawKeyList = rks == null || rks.length <= 0 ? null : Arrays.asList(rks);\n\n\t\t\tString str = getString(request, KEY_STRING);\n\t\t\tString[] sks = StringUtil.split(str);\n\t\t\tstringKeyList = sks == null || sks.length <= 0 ? null : Arrays.asList(sks);\n\t\t\trequest.remove(KEY_STRING);\n\n\t\t\tString trim = getString(request, KEY_TRIM);\n\t\t\tString[] trims = StringUtil.split(trim);\n\t\t\ttrimKeyList = trims == null || trims.length <= 0 ? null : Arrays.asList(trims);\n\t\t\trequest.remove(KEY_TRIM);\n\t\t}\n    }\n\n\t@Override\n\tpublic String getParentPath() {\n\t\treturn parentPath;\n\t}\n\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setParentPath(String parentPath) {\n\t\tthis.parentPath = parentPath;\n\t\treturn this;\n\t}\n\n\tprotected M cache;\n\t@Override\n\tpublic M getCache() {\n\t\treturn cache;\n\t}\n\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setCache(M cache) {\n\t\tthis.cache = cache;\n\t\treturn this;\n\t}\n\n\tprotected int position;\n\tpublic int getPosition() {\n\t\treturn position;\n\t}\n\tpublic AbstractObjectParser<T, M, L> setPosition(int position) {\n\t\tthis.position = position;\n\t\treturn this;\n\t}\n\n\n\tprivate boolean invalidate = false;\n\tpublic void invalidate() {\n\t\tinvalidate = true;\n\t}\n\tpublic boolean isInvalidate() {\n\t\treturn invalidate;\n\t}\n\n\tprivate boolean breakParse = false;\n\tpublic void breakParse() {\n\t\tbreakParse = true;\n\t}\n\tpublic boolean isBreakParse() {\n\t\treturn breakParse || isInvalidate();\n\t}\n\n\tprotected String name;\n\tprotected String table;\n\tprotected String alias;\n\tprotected boolean isReuse;\n\tprotected String path;\n\n\tprotected M response;\n\tprotected M sqlRequest;\n\tprotected M sqlResponse;\n\t/**\n\t * 自定义关键词\n\t */\n\tprotected Map<String, Object> customMap;\n\t/**\n\t * 远程函数\n\t * {\"-\":{ \"key-()\":value }, \"0\":{ \"key()\":value }, \"+\":{ \"key+()\":value } }\n\t * - : 在executeSQL前解析\n\t * 0 : 在executeSQL后、onChildParse前解析\n\t * + : 在onChildParse后解析\n\t */\n\tprotected Map<String, Map<String, String>> functionMap;\n\t/**\n\t * 子对象\n\t */\n\tprotected Map<String, M> childMap;\n\n\tprivate int objectCount;\n\tprivate int arrayCount;\n\n    private List<String> rawKeyList;\n\t/**解析成员\n\t * response重新赋值\n\t * @return null or this\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> parse(String name, boolean isReuse) throws Exception {\n\t\tif (isInvalidate() == false) {\n\t\t\tthis.isReuse = isReuse;\n\t\t\tthis.name = name;\n\t\t\tthis.path = AbstractParser.getAbsPath(parentPath, name);\n\n\t\t\tapijson.orm.Entry<String, String> tentry = Pair.parseEntry(name, true);\n\t\t\tthis.table = tentry.getKey();\n\t\t\tthis.alias = tentry.getValue();\n\n\t\t\tLog.d(TAG, \"AbstractObjectParser<T, M, L>  parentPath = \" + parentPath + \"; name = \" + name + \"; table = \" + table + \"; alias = \" + alias);\n\t\t\tLog.d(TAG, \"AbstractObjectParser<T, M, L>  type = \" + type + \"; isTable = \" + isTable + \"; isArrayMainTable = \" + isArrayMainTable);\n\t\t\tLog.d(TAG, \"AbstractObjectParser<T, M, L>  isEmpty = \" + request.isEmpty() + \"; tri = \" + tri + \"; drop = \" + drop);\n\n\t\t\tbreakParse = false;\n\n\t\t\tresponse = JSON.createJSONObject(); // must init\n\t\t\tsqlResponse = null; // must init\n\n\t\t\tif (isReuse == false) {\n\t\t\t\tsqlRequest = JSON.createJSONObject(); // must init\n\n\t\t\t\tcustomMap = null; // must init\n\t\t\t\tfunctionMap = null; // must init\n\t\t\t\tchildMap = null; // must init\n\n\t\t\t\tSet<Entry<String, Object>> set = request.isEmpty() ? null : new LinkedHashSet<>(request.entrySet());\n\t\t\t\tif (set != null && set.isEmpty() == false) { // 判断换取少几个变量的初始化是否值得？\n\t\t\t\t\tif (isTable) { // 非Table下必须保证原有顺序！否则 count,page 会丢, total@:\"/[]/total\" 会在[]:{}前执行！\n\t\t\t\t\t\tcustomMap = new LinkedHashMap<String, Object>();\n\t\t\t\t\t\tchildMap = new LinkedHashMap<String, M>();\n\t\t\t\t\t}\n\t\t\t\t\tfunctionMap = new LinkedHashMap<String, Map<String, String>>();//必须执行\n\n\t\t\t\t\t// 条件 <<<<<<<<<<<<<<<<<<<\n\t\t\t\t\tList<String> whereList = null;\n\t\t\t\t\tif (method == PUT) { // 这里只有PUTArray需要处理  || method == DELETE) {\n\t\t\t\t\t\tString[] combine = StringUtil.split(getString(request, KEY_COMBINE));\n\t\t\t\t\t\tif (combine != null) {\n\t\t\t\t\t\t\tString w;\n\t\t\t\t\t\t\tfor (int i = 0; i < combine.length; i++) { // 去除 &,|,! 前缀\n\t\t\t\t\t\t\t\tw = combine[i];\n\t\t\t\t\t\t\t\tif (w != null && (w.startsWith(\"&\") || w.startsWith(\"|\") || w.startsWith(\"!\"))) {\n\t\t\t\t\t\t\t\t\tcombine[i] = w.substring(1);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Arrays.asList() 返回值不支持 add 方法！\n\t\t\t\t\t\twhereList = new ArrayList<String>(Arrays.asList(combine != null ? combine : new String[]{}));\n\t\t\t\t\t\twhereList.add(JSONMap.KEY_ID);\n\t\t\t\t\t\twhereList.add(JSONMap.KEY_ID_IN);\n\t\t\t\t\t\t//\t\t\t\t\t\twhereList.add(apijson.JSONMap.KEY_USER_ID);\n\t\t\t\t\t\t//\t\t\t\t\t\twhereList.add(apijson.JSONMap.KEY_USER_ID_IN);\n\t\t\t\t\t}\n\t\t\t\t\t// 条件>>>>>>>>>>>>>>>>>>>\n\n\t\t\t\t\tint index = 0;\n                    // hasOtherKeyNotFun = false;\n\t\t\t\t\tM viceItem = null;\n\n\t\t\t\t\tfor (Entry<String, Object> entry : set) {\n\t\t\t\t\t\tif (isBreakParse()) {\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// key 可能为 JSONList，需要进行手动转换（fastjson 为低版本时允许自动转换，如 1.2.21）\n\t\t\t\t\t\t// 例如 request json为 \"{[]:{\"page\": 2, \"table1\":{}}}\"\n\t\t\t\t\t\tObject field = entry == null ? null : entry.getKey();\n\t\t\t\t\t\tString key = field instanceof Map ? toJSONString(field) : field.toString();\n\t\t\t\t\t\tObject value = key == null ? null : entry.getValue();\n\t\t\t\t\t\tif (value == null) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n                        // 处理url crud, 将crud 转换为真实method\n                        RequestMethod _method = this.parser.getRealMethod(method, key, value);\n\t\t\t\t\t\t// 没有执行校验流程的情况,比如url head, sql@子查询, sql@ method=GET\n\n\t\t\t\t\t\tObject obj = key.endsWith(\"@\") ? request.get(key) : null;\n\t\t\t\t\t\tif (obj instanceof Map<?, ?>) {\n\t\t\t\t\t\t\t((Map<String, Object>) obj).put(JSONMap.KEY_METHOD, GET);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\ttry {\n                            boolean startsWithAt = key.startsWith(\"@\");\n                            // if (startsWithAt || (key.endsWith(\"()\") == false)) {\n                            //     hasOtherKeyNotFun = true;\n                            // }\n\n\t\t\t\t\t\t\tboolean isTrim = (trimKeyList != null && trimKeyList.contains(key));\n\t\t\t\t\t\t\tboolean toStr = isTrim || (stringKeyList != null && stringKeyList.contains(key));\n\t\t\t\t\t\t\tif (toStr) {\n\t\t\t\t\t\t\t\tvalue = JSON.toJSONString(value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (isTrim && value != null) {\n\t\t\t\t\t\t\t\tvalue = StringUtil.trim(value);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (startsWithAt || key.endsWith(\"@\") || (key.endsWith(\"<>\") && value instanceof Map<?, ?>)) {\n\t\t\t\t\t\t\t\tif (onParse(key, value) == false) {\n\t\t\t\t\t\t\t\t\tinvalidate();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (value instanceof Map<?, ?>) {  // JSONRequest，往下一级提取\n\t\t\t\t\t\t\t\tif (childMap != null) {  // 添加到childMap，最后再解析\n\t\t\t\t\t\t\t\t\tchildMap.put(key, (M) value);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse {  // 直接解析并替换原来的，[]:{} 内必须直接解析，否则会因为丢掉count等属性，并且total@:\"/[]/total\"必须在[]:{} 后！\n\t\t\t\t\t\t\t\t\tObject cache = index <= 0 || type != TYPE_ITEM || viceItem == null ? null : JSON.get(viceItem, key);\n\t\t\t\t\t\t\t\t\tObject result = onChildParse(index, key, (M) value, cache);\n\t\t\t\t\t\t\t\t\tif (index <= 0 && type == TYPE_ITEM) {\n\t\t\t\t\t\t\t\t\t\tM mainItem = (M) result;\n\t\t\t\t\t\t\t\t\t\tviceItem = result == null ? null : (M) mainItem.remove(AbstractSQLExecutor.KEY_VICE_ITEM);\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tresponse.put(key, result);\n\t\t\t\t\t\t\t\t\tindex ++;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if ((_method == POST || _method == PUT) && value instanceof List<?>\n\t\t\t\t\t\t\t\t\t&& JSONMap.isTableArray(key)) {  // L，批量新增或修改，往下一级提取\n\t\t\t\t\t\t\t\tonTableArrayParse(key, (L) value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (_method == PUT && value instanceof List<?> && (whereList == null || whereList.contains(key) == false)\n\t\t\t\t\t\t\t\t\t&& StringUtil.isName(key.replaceFirst(\"[+-]$\", \"\"))) {  // PUT L\n\t\t\t\t\t\t\t\tonPUTArrayParse(key, (L) value);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {  // L 或其它 Object，直接填充\n\t\t\t\t\t\t\t\tif (onParse(key, value) == false) {\n\t\t\t\t\t\t\t\t\tinvalidate();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\t\tif (tri == false) {\n\t\t\t\t\t\t\t\tthrow CommonException.wrap(e, sqlConfig);  // 不忽略错误，抛异常\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tinvalidate();  // 忽略错误，还原request\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t}\n\n\t\t\t\tif (isTable) {\n\t\t\t\t\t// parser.onVerifyRole 已处理 globalRole\n\n\t\t\t\t\tString db = parser.getGlobalDatabase();\n\t\t\t\t\tif (db != null) {\n\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_DATABASE, db);\n\t\t\t\t\t}\n\n\t\t\t\t\tString ds = parser.getGlobalDatasource();\n\t\t\t\t\tif (ds != null) {\n\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_DATASOURCE, ds);\n\t\t\t\t\t}\n\n\t\t\t\t\tString ns = parser.getGlobalNamespace();\n\t\t\t\t\tif (ns != null) {\n\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_NAMESPACE, ns);\n\t\t\t\t\t}\n\n\t\t\t\t\tString cl = parser.getGlobalCatalog();\n\t\t\t\t\tif (cl != null) {\n\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_CATALOG, cl);\n\t\t\t\t\t}\n\n\t\t\t\t\tString sch = parser.getGlobalSchema();\n\t\t\t\t\tif (sch != null) {\n\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_SCHEMA, sch);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (isSubquery == false) { // 解决 SQL 语法报错，子查询不能 EXPLAIN\n\t\t\t\t\t\tBoolean exp = parser.getGlobalExplain();\n\t\t\t\t\t\tif (sch != null) {\n\t\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_EXPLAIN, exp);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tString cache = parser.getGlobalCache();\n\t\t\t\t\t\tif (cache != null) {\n\t\t\t\t\t\t\tsqlRequest.putIfAbsent(JSONMap.KEY_CACHE, cache);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isTable) {  // 非Table内的函数会被滞后在onChildParse后调用\n\t\t\t\tonFunctionResponse(\"-\");\n\t\t\t}\n\n\t\t}\n\n\t\tif (isInvalidate()) {\n\t\t\trecycle();\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this;\n\t}\n\n\n\n\n    //private boolean hasOtherKeyNotFun = false;\n\n    /**解析普通成员\n\t * @param key\n\t * @param value\n\t * @return whether parse succeed\n\t */\n\t@Override\n\tpublic boolean onParse(@NotNull String key, @NotNull Object value) throws Exception {\n\t\tif (key.endsWith(\"@\")) {  // StringUtil.isPath((String) value)) {\n\t\t\t// [] 内主表 position > 0 时，用来生成 SQLConfig<T, M, L> 的键值对全都忽略，不解析\n\t\t\tif (value instanceof Map<?, ?>) {  // key{}@ getRealKey, SQL 子查询对象，JSONRequest -> SQLConfig.getSQL\n\t\t\t\tString replaceKey = key.substring(0, key.length() - 1);\n\n\t\t\t\tM subquery = (M) value;\n\t\t\t\tString range = getString(subquery, KEY_SUBQUERY_RANGE);\n\t\t\t\tif (range != null && SUBQUERY_RANGE_ALL.equals(range) == false && SUBQUERY_RANGE_ANY.equals(range) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"子查询 \" + path + \"/\" + key + \":{ range:value } 中 value 只能为 [\"\n                            + SUBQUERY_RANGE_ALL + \", \" + SUBQUERY_RANGE_ANY + \"] 中的一个！\");\n\t\t\t\t}\n\n\t\t\t\tL arr = parser.onArrayParse(subquery, path, key, true, null);\n\n\t\t\t\tM obj = arr == null || arr.isEmpty() ? null : JSON.get(arr, 0);\n\t\t\t\tif (obj == null) {\n\t\t\t\t\tthrow new Exception(\"服务器内部错误，解析子查询 \" + path + \"/\" + key + \":{ } 为 Subquery 对象失败！\");\n\t\t\t\t}\n\n\t\t\t\tString from = getString(subquery, apijson.JSONRequest.KEY_SUBQUERY_FROM);\n\t\t\t\tboolean isEmpty = StringUtil.isEmpty(from);\n\t\t\t\tM arrObj = isEmpty ? null : JSON.get(obj, from);\n\t\t\t\tif (isEmpty) {\n\t\t\t\t\tSet<Entry<String, Object>> set = obj.entrySet();\n\t\t\t\t\tfor (Entry<String, Object> e : set) {\n\t\t\t\t\t\tString k = e == null ? null : e.getKey();\n\t\t\t\t\t\tObject v = k == null ? null : e.getValue();\n\t\t\t\t\t\tif (v instanceof Map<?, ?> && JSONMap.isTableKey(k)) {\n\t\t\t\t\t\t\tfrom = k;\n\t\t\t\t\t\t\tarrObj = (M) v;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (arrObj == null) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"子查询 \" + path + \"/\"\n                            + key + \":{ from:value } 中 value 对应的主表对象 \" + from + \":{} 不存在！\");\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> cfg = (SQLConfig) arrObj.get(AbstractParser.KEY_CONFIG);\n\t\t\t\tif (cfg == null) {\n\t\t\t\t\tthrow new NotExistException(TAG + \".onParse  cfg == null\");\n\t\t\t\t}\n\n\t\t\t\tSubquery s = new Subquery();\n\t\t\t\ts.setPath(path);\n\t\t\t\ts.setOriginKey(key);\n\t\t\t\ts.setOriginValue(subquery);\n\n\t\t\t\ts.setFrom(from);\n\t\t\t\ts.setRange(range);\n\t\t\t\ts.setKey(replaceKey);\n\t\t\t\ts.setConfig(cfg);\n\n\t\t\t\tkey = replaceKey;\n\t\t\t\tvalue = s; //(range == null || range.isEmpty() ? \"\" : \"range\") + \"(\" + cfg.getSQL(false) + \") \";\n\n\t\t\t\tparser.putQueryResult(AbstractParser.getAbsPath(path, key), s); //字符串引用保证不了安全性 parser.getSQL(cfg));\n\t\t\t}\n\t\t\telse if (value instanceof String) { // //key{}@ getRealKey, 引用赋值路径\n\t\t\t\tString replaceKey = key.substring(0, key.length() - 1);\n\n\t\t\t\t// System.out.println(\"getObject  key.endsWith(@) >> parseRelation = \" + parseRelation);\n\t\t\t\tString targetPath = AbstractParser.getValuePath(type == TYPE_ITEM ? path : parentPath, (String) value);\n\n\t\t\t\t// 先尝试获取，尽量保留缺省依赖路径，这样就不需要担心路径改变\n\t\t\t\tObject target = onReferenceParse(targetPath);\n\t\t\t\tLog.i(TAG, \"onParse targetPath = \" + targetPath + \"; target = \" + target);\n\n\t\t\t\tif (target == null) { // String#equals(null)会出错\n\t\t\t\t\tLog.d(TAG, \"onParse  target == null  >>  return true;\");\n\n\t\t\t\t\tif (Log.DEBUG) {\n\t\t\t\t\t\tparser.putWarnIfNeed(AbstractParser.KEY_REF, path + \"/\" + key + \": \" + targetPath + \" 引用赋值获取路径对应的值为 null！请检查路径是否错误！\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// 非查询关键词 @key 不影响查询，直接跳过\n\t\t\t\t\tif (isTable && (key.startsWith(\"@\") == false || JSONMap.TABLE_KEY_LIST.contains(key))) {\n\t\t\t\t\t\tLog.e(TAG, \"onParse  isTable && (key.startsWith(@) == false\"\n\t\t\t\t\t\t\t\t+ \" || apijson.JSONMap.TABLE_KEY_LIST.contains(key)) >>  return null;\");\n\t\t\t\t\t\t// FIXME getCache() != null 时 return true，解决 RIGHT/OUTER/FOREIGN JOIN 主表无数据导致副表数据也不返回\n\t\t\t\t\t\treturn false; // 获取不到就不用再做无效的 query 了。不考虑 Table:{Table:{}} 嵌套\n\t\t\t\t\t}\n\n\t\t\t\t\tLog.d(TAG, \"onParse  isTable(table) == false >> return true;\");\n\t\t\t\t\treturn true; // 舍去，对Table无影响\n\t\t\t\t}\n\n//\t\t\t\tif (target instanceof Map) { // target 可能是从 requestObject 里取出的 {}\n//\t\t\t\t\tif (isTable || targetPath.endsWith(\"[]/\" + JSONResponse.KEY_INFO) == false) {\n//\t\t\t\t\t\tLog.d(TAG, \"onParse  target instanceof Map  >>  return false;\");\n//\t\t\t\t\t\treturn false; // FIXME 这个判断现在来看是否还有必要？为啥不允许为 JSONRequest ？以前可能因为防止二次遍历再解析，现在只有一次遍历\n//\t\t\t\t\t}\n//\t\t\t\t}\n//\n//\t\t\t\t// FIXME 这个判断现在来看是否还有必要？为啥不允许为 JSONRequest ？以前可能因为防止二次遍历再解析，现在只有一次遍历\n//\t\t\t\tif (targetPath.equals(target)) { // 必须 valuePath 和保证 getValueByPath 传进去的一致！\n//\t\t\t\t\tLog.d(TAG, \"onParse  targetPath.equals(target)  >>\");\n//\n//\t\t\t\t\t//非查询关键词 @key 不影响查询，直接跳过\n//\t\t\t\t\tif (isTable && (key.startsWith(\"@\") == false || apijson.JSONMap.TABLE_KEY_LIST.contains(key))) {\n//\t\t\t\t\t\tLog.e(TAG, \"onParse  isTable && (key.startsWith(@) == false\"\n//\t\t\t\t\t\t\t\t+ \" || apijson.JSONMap.TABLE_KEY_LIST.contains(key)) >>  return null;\");\n//\t\t\t\t\t\treturn false;//获取不到就不用再做无效的query了。不考虑 Table:{Table:{}}嵌套\n//\t\t\t\t\t} else {\n//\t\t\t\t\t\tLog.d(TAG, \"onParse  isTable(table) == false >> return true;\");\n//\t\t\t\t\t\treturn true;//舍去，对Table无影响\n//\t\t\t\t\t}\n//\t\t\t\t}\n\n\t\t\t\t// 直接替换原来的 key@: path 为 key: target\n\t\t\t\tLog.i(TAG, \"onParse    >>  key = replaceKey; value = target;\");\n\t\t\t\tkey = replaceKey;\n\t\t\t\tvalue = target;\n\t\t\t\tLog.d(TAG, \"onParse key = \" + key + \"; value = \" + value);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow new IllegalArgumentException(path + \"/\" + key + \":value 中 value 必须为 依赖路径String 或 SQL子查询JSONObject ！\");\n\t\t\t}\n\t\t}\n\n\t\tif (key.endsWith(\"()\")) {\n\t\t\tif (value instanceof String == false) {\n\t\t\t\tthrow new IllegalArgumentException(path + \"/\" + key + \":value 中 value 必须为函数String！\");\n\t\t\t}\n\n\t\t\tString k = key.substring(0, key.length() - 2);\n\n\t\t\tString type; //远程函数比较少用，一般一个Table:{}内用到也就一两个，所以这里用 \"-\",\"0\",\"+\" 更直观，转用 -1,0,1 对性能提升不大。\n\t\t\tboolean isMinus = k.endsWith(\"-\");\n            boolean isPlus = isMinus == false && k.endsWith(\"+\");\n\t\t\tif (isMinus) { //不能封装到functionMap后批量执行，否则会导致非Table内的 key-():function() 在onChildParse后执行！\n\t\t\t\ttype = \"-\";\n\t\t\t\tk = k.substring(0, k.length() - 1);\n\t\t\t}\n\t\t\telse if (isPlus) {\n\t\t\t\ttype = \"+\";\n\t\t\t\tk = k.substring(0, k.length() - 1);\n\t\t\t}\n\t\t\telse {\n\t\t\t\ttype = \"0\";\n\t\t\t}\n\n            if (isPlus == false && isTable == false) {\n                parseFunction(key, k, (String) value, this.type == TYPE_ITEM ? path : parentPath, name, request, isMinus);\n            }\n            else {\n\t\t\t\t//远程函数比较少用，一般一个Table:{}内用到也就一两个，所以这里循环里new出来对性能影响不大。\n\t\t\t\tMap<String, String> map = functionMap.get(type);\n\t\t\t\tif (map == null) {\n\t\t\t\t\tmap = new LinkedHashMap<>();\n\t\t\t\t}\n\t\t\t\tmap.put(k, (String) value);\n\n\t\t\t\tfunctionMap.put(type, map);\n\t\t\t}\n\t\t}\n\t\telse if (isTable && key.startsWith(\"@\") && JSONMap.TABLE_KEY_LIST.contains(key) == false) {\n            customMap.put(key, value);\n\t\t}\n\t\telse {\n\t\t\tsqlRequest.put(key, value);\n\t\t}\n\n\t\treturn true;\n\t}\n\n\n\n\t/**\n\t * @param index\n\t * @param key\n\t * @param value\n\t * @param cache\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic Object onChildParse(int index, String key, M value, Object cache) throws Exception {\n\t\tboolean isFirst = index <= 0;\n\t\tboolean isMain = isFirst && type == TYPE_ITEM;\n\n\t\tObject child;\n\t\tboolean isEmpty;\n\n\t\tif (JSONMap.isArrayKey(key)) { // APIJSON Array\n\t\t\tif (isMain) {\n\t\t\t\tthrow new IllegalArgumentException(parentPath + \"/\" + key + \":{} 不合法！\"\n\t\t\t\t\t\t+ \"数组 []:{} 中第一个 key:{} 必须是主表 TableKey:{} ！不能为 arrayKey[]:{} ！\");\n\t\t\t}\n\n\t\t\tif (arrayConfig == null || arrayConfig.getPosition() == 0) {\n\t\t\t\tarrayCount ++;\n\t\t\t\tint maxArrayCount = parser.getMaxArrayCount();\n\t\t\t\tif (arrayCount > maxArrayCount) {\n\t\t\t\t\tthrow new IllegalArgumentException(path + \" 内截至 \" + key + \":{} 时数组对象 key[]:{} \"\n                            + \"的数量达到 \" + arrayCount + \" 已超限，必须在 0-\" + maxArrayCount + \" 内 !\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tString query = getString(value, KEY_QUERY);\n\t\t\tchild = parser.onArrayParse(value, path, key, isSubquery, cache instanceof List<?> ? (L) cache : null);\n\t\t\tisEmpty = child == null || ((List<?>) child).isEmpty();\n\n\t\t\tif (\"2\".equals(query) || \"ALL\".equals(query)) { // 不判断 isEmpty，因为分页数据可能只是某页没有\n\t\t\t\tString totalKey = JSONResponse.formatArrayKey(key) + \"Total\";\n\t\t\t\tString infoKey = JSONResponse.formatArrayKey(key) + \"Info\";\n\t\t\t\tif ((request.containsKey(totalKey) || request.containsKey(infoKey)\n\t\t\t\t\t\t|| request.containsKey(totalKey + \"@\") || request.containsKey(infoKey + \"@\")) == false) {\n\t\t\t\t\t// onParse(\"total@\", \"/\" + key + \"/total\");\n\t\t\t\t\t// onParse(infoKey + \"@\", \"/\" + key + \"/info\");\n\t\t\t\t\t// 替换为以下性能更好、对流程干扰最小的方式：\n\n\t\t\t\t\tString keyPath = AbstractParser.getValuePath(type == TYPE_ITEM ? path : parentPath, \"/\" + key);\n\t\t\t\t\tString totalPath = keyPath + \"/total\";\n\t\t\t\t\tString infoPath = keyPath + \"/info\";\n\t\t\t\t\tresponse.put(totalKey, onReferenceParse(totalPath));\n\t\t\t\t\tresponse.put(infoKey, onReferenceParse(infoPath));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse { //APIJSON Object\n\t\t\tboolean isTableKey = JSONMap.isTableKey(Pair.parseEntry(key, true).getKey());\n\t\t\tif (type == TYPE_ITEM && isTableKey == false) {\n\t\t\t\tthrow new IllegalArgumentException(parentPath + \"/\" + key + \":{} 不合法！\"\n\t\t\t\t\t\t+ \"数组 []:{} 中每个 key:{} 都必须是表 TableKey:{} 或 数组 arrayKey[]:{} ！\");\n\t\t\t}\n\n\t\t\tif ( //避免使用 \"test\":{\"Test\":{}} 绕过限制，实现查询爆炸   isTableKey &&\n\t\t\t\t\t(arrayConfig == null || arrayConfig.getPosition() == 0)) {\n\t\t\t\tobjectCount ++;\n\t\t\t\tint maxObjectCount = parser.getMaxObjectCount();\n\t\t\t\tif (objectCount > maxObjectCount) {  //TODO 这里判断是批量新增/修改，然后上限为 maxUpdateCount\n\t\t\t\t\tthrow new IllegalArgumentException(path + \" 内截至 \" + key + \":{} 时对象\"\n\t\t\t\t\t\t\t+ \" key:{} 的数量达到 \" + objectCount + \" 已超限，必须在 0-\" + maxObjectCount + \" 内 !\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchild = parser.onObjectParse(value, path, key, isMain ? arrayConfig.setType(SQLConfig.TYPE_ITEM_CHILD_0) : null\n\t\t\t\t\t, isSubquery, cache instanceof Map<?, ?> ? (M) cache : null);\n\n\t\t\tisEmpty = child == null || ((Map<?, ?>) child).isEmpty();\n\t\t\tif (isFirst && isEmpty) {\n\t\t\t\tinvalidate();\n\t\t\t}\n\t\t}\n//\t\tLog.i(TAG, \"onChildParse  ObjectParser.onParse  key = \" + key + \"; child = \" + child);\n\n\t\treturn isEmpty ? null : child; // 只添加! isChildEmpty的值，可能数据库返回数据不够count\n\t}\n\n\n\t//TODO 改用 MySQL json_add,json_remove,json_contains 等函数！不过就没有具体报错了，或许可以新增功能符，或者直接调 SQL 函数\n\n\t/**PUT key:[]\n\t * @param key\n\t * @param array\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic void onPUTArrayParse(@NotNull String key, @NotNull L array) throws Exception {\n\t\tif (isTable == false || array.isEmpty()) {\n\t\t\tsqlRequest.put(key, array);\n\t\t\tLog.e(TAG, \"onPUTArrayParse  isTable == false || array == null || array.isEmpty() >> return;\");\n\t\t\treturn;\n\t\t}\n\n\t\tint putType = 0;\n\t\tif (key.endsWith(\"+\")) {//add\n\t\t\tputType = 1;\n\t\t} else if (key.endsWith(\"-\")) {//remove\n\t\t\tputType = 2;\n\t\t} else {//replace\n\t\t\tsqlRequest.put(key, array);\n\t\t\treturn;\n\t\t}\n\t\tString realKey = AbstractSQLConfig.gainRealKey(method, key, false, false);\n\n\t\t//GET > add all 或 remove all > PUT > remove key\n\n\t\t//GET <<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tM rq = JSON.createJSONObject();\n\t\trq.put(JSONMap.KEY_ID, request.get(JSONMap.KEY_ID));\n\t\trq.put(JSONMap.KEY_COLUMN, realKey);\n\t\tM rp = parseResponse(RequestMethod.GET, table, null, rq, null, false);\n\t\t//GET >>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t\t//add all 或 remove all <<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tObject target = rp == null ? null : rp.get(realKey);\n\t\tif (target instanceof String) {\n\t\t\ttry {\n\t\t\t\ttarget = JSON.parse(target);\n\t\t\t} catch (Throwable e) {\n\t\t\t\tif (Log.DEBUG) {\n\t\t\t\t\tLog.e(TAG, \"try {\\n\" +\n\t\t\t\t\t\t\t\"\\t\\t\\t\\ttarget = parseJSON((String) target);\\n\" +\n\t\t\t\t\t\t\t\"\\t\\t\\t}\\n\" +\n\t\t\t\t\t\t\t\"\\t\\t\\tcatch (Throwable e) = \" + e.getMessage());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (apijson.JSON.isBoolOrNumOrStr(target)) {\n\t\t\tthrow new NullPointerException(\"PUT \" + path + \", \" + realKey + \" 类型为 \" + target.getClass().getSimpleName() + \"，\"\n\t\t\t\t\t+ \"不支持 Boolean, String, Number 等类型字段使用 'key+': [] 或 'key-': [] ！\"\n\t\t\t\t\t+ \"对应字段在数据库的值必须为 L, JSONRequest 中的一种！\"\n\t\t\t\t\t+ \"值为 JSONRequest 类型时传参必须是 'key+': [{'key': value, 'key2': value2}] 或 'key-': ['key', 'key2'] ！\"\n\t\t\t);\n\t\t}\n\n\t\tboolean isAdd = putType == 1;\n\n\t\tCollection<Object> targetArray = target instanceof Collection ? (Collection<Object>) target : null;\n\t\tMap<String, ?> targetObj = target instanceof Map ? (Map<String, Object>) target : null;\n\n\t\tif (targetArray == null && targetObj == null) {\n\t\t\tif (isAdd == false) {\n\t\t\t\tthrow new NullPointerException(\"PUT \" + path + \", \" + realKey + (target == null ? \" 值为 null，不支持移除！\"\n\t\t\t\t\t\t: \" 类型为 \" + target.getClass().getSimpleName() + \"，不支持这样移除！\")\n\t\t\t\t\t\t+ \"对应字段在数据库的值必须为 L, JSONRequest 中的一种，且 key- 移除时，本身的值不能为 null！\"\n\t\t\t\t\t\t+ \"值为 JSONRequest 类型时传参必须是 'key+': [{'key': value, 'key2': value2}] 或 'key-': ['key', 'key2'] ！\"\n\t\t\t\t);\n\t\t\t}\n\n\t\t\ttargetArray = JSON.createJSONArray();\n\t\t}\n\n\t\tfor (int i = 0; i < array.size(); i++) {\n\t\t\tObject obj = array.get(i);\n\t\t\tif (obj == null) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (isAdd) {\n\t\t\t\tif (targetArray != null) {\n\t\t\t\t\tif (targetArray.contains(obj)) {\n\t\t\t\t\t\tthrow new ConflictException(\"PUT \" + path + \", \" + key + \"/\" + i + \" 已存在！\");\n\t\t\t\t\t}\n\t\t\t\t\ttargetArray.add(obj);\n\t\t\t\t} else {\n\t\t\t\t\tif (obj != null && obj instanceof Map == false) {\n\t\t\t\t\t\tthrow new ConflictException(\"PUT \" + path + \", \" + key + \"/\" + i + \" 必须为 JSONRequest {} ！\");\n\t\t\t\t\t}\n\t\t\t\t\ttargetObj.putAll((Map) obj);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (targetArray != null) {\n\t\t\t\t\tif (targetArray.contains(obj) == false) {\n\t\t\t\t\t\tthrow new NullPointerException(\"PUT \" + path + \", \" + key + \"/\" + i + \" 不存在！\");\n\t\t\t\t\t}\n\t\t\t\t\ttargetArray.remove(obj);\n\t\t\t\t} else {\n\t\t\t\t\tif (obj instanceof String == false) {\n\t\t\t\t\t\tthrow new ConflictException(\"PUT \" + path + \", \" + key + \"/\" + i + \" 必须为 String 类型 ！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (targetObj.containsKey(obj) == false) {\n\t\t\t\t\t\tthrow new NullPointerException(\"PUT \" + path + \", \" + key + \"/\" + i + \" 不存在！\");\n\t\t\t\t\t}\n\t\t\t\t\ttargetObj.remove(obj);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t//add all 或 remove all >>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t\t//PUT <<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tsqlRequest.put(realKey, targetArray != null ? targetArray : JSON.toJSONString(targetObj)); // FIXME, SerializerFeature.WriteMapNullValue));\n\t\t//PUT >>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t}\n\n\n\t@Override\n\tpublic void onTableArrayParse(String key, L valueArray) throws Exception {\n\t\tString childKey = key.substring(0, key.length() - JSONMap.KEY_ARRAY.length());\n\n\t\tint allCount = 0;\n\t\tL ids = JSON.createJSONArray();\n\n\t\tint version = parser.getVersion();\n\t\tint maxUpdateCount = parser.getMaxUpdateCount();\n\n        SQLConfig<T, M, L> cfg = null; // 不能污染当前的配置 getSQLConfig();\n        if (cfg == null) { // TODO 每次都创建成本比较高，是否新增 defaultInstance 或者 configInstance 用来专门 getIdKey 等？\n            cfg = parser.createSQLConfig();\n        }\n\n\t\tString idKey = cfg.getIdKey(); //Table[]: [{}] arrayConfig 为 null\n\t\tboolean isNeedVerifyContent = parser.isNeedVerifyContent();\n\n        cfg.setTable(childKey); // Request 表 structure 中配置 \"ALLOW_PARTIAL_UPDATE_FAILED\": \"Table[],key[],key:alias[]\" 自动配置\n        boolean allowPartialFailed = cfg.allowPartialUpdateFailed();\n        L failedIds = allowPartialFailed ? JSON.createJSONArray() : null;\n\n        int firstFailIndex = -1;\n        M firstFailReq = null;\n        Throwable firstFailThrow = null;\n\t\tfor (int i = 0; i < valueArray.size(); i++) { //只要有一条失败，则抛出异常，全部失败\n\t\t\t//TODO 改成一条多 VALUES 的 SQL 性能更高，报错也更会更好处理，更人性化\n\t\t\tM item;\n\t\t\ttry {\n\t\t\t\titem = JSON.get(valueArray, i);\n                if (item == null) {\n                    throw new NullPointerException();\n                }\n\t\t\t}\n\t\t\tcatch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(\n                        \"批量新增/修改失败！\" + key + \"/\" + i + \":value 中value不合法！类型必须是 OBJECT ，结构为 {} !\"\n                );\n\t\t\t}\n\n            Object id = item.get(idKey);\n\t\t\tM req = JSON.createJSONObject(childKey, item);\n\n            M result = null;\n            try {\n                if (isNeedVerifyContent) {\n                    req = parser.parseCorrectRequest(method, childKey, version, \"\", req, maxUpdateCount, parser);\n                }\n                //parser.getMaxSQLCount() ? 可能恶意调用接口，把数据库拖死\n                result = (M) onChildParse(0, \"\" + i, req, null);\n            }\n            catch (Exception e) {\n                if (allowPartialFailed == false) {\n                    throw e;\n                }\n\n                if (firstFailThrow == null) {\n                    firstFailThrow = e;\n                    firstFailReq = JSON.get(valueArray, i); // item\n                }\n            }\n\n            result = result == null ? null : JSON.get(result, childKey);\n\n            boolean success = JSONResponse.isSuccess(result);\n            int count = result == null ? 0 : getIntValue(result, JSONResponse.KEY_COUNT);\n            if (id == null && result != null) {\n                id = result.get(idKey);\n            }\n\n            if (success == false || count != 1) { //如果 code = 200 但 count != 1，不能算成功，掩盖了错误不好排查问题\n                if (allowPartialFailed) {\n                    failedIds.add(id);\n                    if (firstFailIndex < 0) {\n                        firstFailIndex = i;\n                    }\n                }\n                else {\n                    throw new ServerException(\n                            \"批量新增/修改失败！\" + key + \"/\" + i + \"：\" + (success ? \"成功但 count != 1 ！\"\n                                    : (result == null ? \"null\" : getString(result, JSONResponse.KEY_MSG))\n                    ));\n                }\n            }\n\n\t\t\tallCount += 1; // 加了 allowPartialFailed 后 count 可能为 0  allCount += count;\n\t\t\tids.add(id);\n\t\t}\n\n        int failedCount = failedIds == null ? 0 : failedIds.size();\n        if (failedCount > 0 && failedCount >= allCount) {\n            throw new ServerException(\"批量新增/修改 \" + key + \":[] 中 \" + allCount + \" 个子项全部失败！\"\n                    + \"第 \" + firstFailIndex + \" 项失败原因：\" + (firstFailThrow == null ? \"\" : firstFailThrow.getMessage()));\n        }\n\n        M allResult = getParser().newSuccessResult();\n        if (failedCount > 0) {\n            allResult.put(\"failedCount\", failedCount);\n            allResult.put(\"failedIdList\", failedIds);\n\n            M failObj = JSON.createJSONObject();\n            failObj.put(\"index\", firstFailIndex);\n            failObj.put(childKey, firstFailReq);\n\n            if (firstFailThrow instanceof CommonException && firstFailThrow.getCause() != null) {\n                firstFailThrow = firstFailThrow.getCause();\n            }\n            M obj = firstFailThrow == null ? failObj : getParser().extendErrorResult(failObj, firstFailThrow, parser.isRoot());\n            if (Log.DEBUG && firstFailThrow != null) {\n                obj.put(\"trace:throw\", firstFailThrow.getClass().getName());\n                obj.put(\"trace:stack\", firstFailThrow.getStackTrace());\n            }\n\n            allResult.put(\"firstFailed\", obj);\n        }\n        allResult.put(JSONResponse.KEY_COUNT, allCount);\n        allResult.put(idKey + \"[]\", ids);\n\n\t\tresponse.put(childKey, allResult); //不按原样返回，避免数据量过大\n\t}\n\n\n\t@Override\n\tpublic M parseResponse(RequestMethod method, String table, String alias\n            , M request, List<Join<T, M, L>> joinList, boolean isProcedure) throws Exception {\n\t\tSQLConfig<T, M, L> config = newSQLConfig(method, table, alias, request, joinList, isProcedure)\n\t\t\t\t.setParser(getParser())\n\t\t\t\t.setObjectParser(this);\n\t\treturn parseResponse(config, isProcedure);\n\t}\n\t@Override\n\tpublic M parseResponse(SQLConfig<T, M, L> config, boolean isProcedure) throws Exception {\n\t\tparser = getParser();\n\t\tif (parser.getSQLExecutor() == null) {\n\t\t\tparser.createSQLExecutor();\n\t\t}\n\t\tif (config.gainParser() == null) {\n\t\t\tconfig.setParser(parser);\n\t\t}\n\t\treturn parser.getSQLExecutor().execute(config, isProcedure);\n\t}\n\n\n\t@Override\n\tpublic SQLConfig<T, M, L> newSQLConfig(boolean isProcedure) throws Exception {\n\t\tString raw = Log.DEBUG == false || sqlRequest == null ? null : getString(sqlRequest, JSONMap.KEY_RAW);\n\t\tString[] keys = raw == null ? null : StringUtil.split(raw);\n\t\tif (keys != null && keys.length > 0) {\n\t\t\tboolean allow = AbstractSQLConfig.ALLOW_MISSING_KEY_4_COMBINE;\n\n\t\t\tfor (String key : keys) {\n\t\t\t\tif (sqlRequest.get(key) != null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tString msg = \"@raw:value 的 value 中 \" + key + \" 不合法！对应的 \"\n\t\t\t\t\t\t+ key + \": value 在当前对象 \" + name + \" 不存在或 value = null，无法有效转为原始 SQL 片段！\";\n\n\t\t\t\tif (allow == false) {\n\t\t\t\t\tthrow new UnsupportedOperationException(msg);\n\t\t\t\t}\n\n\t\t\t\tif (parser instanceof AbstractParser) {\n\t\t\t\t\t((AbstractParser) parser).putWarnIfNeed(JSONMap.KEY_RAW, msg);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn newSQLConfig(method, table, alias, sqlRequest, joinList, isProcedure)\n\t\t\t\t.setParser(parser)\n\t\t\t\t.setObjectParser(this);\n\t}\n\n\t/**SQL 配置，for single object\n\t * @return {@link #setSQLConfig(int, int, int)}\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setSQLConfig() throws Exception {\n\t\treturn setSQLConfig(RequestMethod.isQueryMethod(method) ? 1 : 0, 0, 0);\n\t}\n\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setSQLConfig(int count, int page, int position) throws Exception {\n\t\tif (isTable == false || isReuse) {\n\t\t\treturn setPosition(position);\n\t\t}\n\n\t\tif (sqlConfig == null) {\n\t\t\ttry {\n\t\t\t\tsqlConfig = newSQLConfig(false);\n\t\t\t}\n\t\t\tcatch (Exception e) {\n\t\t\t\tif (e instanceof NotExistException || (e instanceof CommonException && e.getCause() instanceof NotExistException)) {\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t\tsqlConfig.setCount(sqlConfig.getCount() <= 0 ? count : sqlConfig.getCount()).setPage(page).setPosition(position);\n\n\t\tparser.onVerifyRole(sqlConfig);\n\n\t\treturn this;\n\t}\n\n\n\n\n\tprotected SQLConfig<T, M, L> sqlConfig = null;//array item复用\n\t/**SQL查询，for array item\n\t * @return this\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> executeSQL() throws Exception {\n\t\t//执行SQL操作数据库\n\t\tif (isTable == false) {//提高性能\n\t\t\tsqlResponse = JSON.createJSONObject();\n\t\t\tsqlResponse.putAll(sqlRequest);\n\t\t}\n\t\telse {\n            try {\n                sqlResponse = onSQLExecute();\n            }\n            catch (Exception e) {\n                if (e instanceof NotExistException || (e instanceof CommonException && e.getCause() instanceof NotExistException)) {\n                    //\t\t\t\tLog.e(TAG, \"getObject  try { response = getSQLObject(config2); } catch (Exception e) {\");\n                    //\t\t\t\tif (e instanceof NotExistException) {//非严重异常，有时候只是数据不存在\n                    //\t\t\t\t\t//\t\t\t\t\t\te.printStackTrace();\n                    sqlResponse = null;//内部吃掉异常，put到最外层\n                    //\t\t\t\t\t\trequestObject.put(JSONResponse.KEY_MSG\n                    //\t\t\t\t\t\t\t\t, StringUtil.getString(requestObject.get(JSONResponse.KEY_MSG)\n                    //\t\t\t\t\t\t\t\t\t\t+ \"; query \" + path + \" cath NotExistException:\"\n                    //\t\t\t\t\t\t\t\t\t\t+ newErrorResult(e).getString(JSONResponse.KEY_MSG)));\n                    //\t\t\t\t} else {\n                    //\t\t\t\t\tthrow e;\n                    //\t\t\t\t}\n                }\n                else {\n                    throw e;\n                }\n            }\n        }\n\n        if (drop) {//丢弃Table，只为了向下提供条件\n            sqlResponse = null;\n        }\n\n        return this;\n\t}\n\n\t/**\n\t * @return response\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic M response() throws Exception {\n\t\tif (sqlResponse == null || sqlResponse.isEmpty()) {\n\t\t\tif (isTable) {//Table自身都获取不到值，则里面的Child都无意义，不需要再解析\n\t\t\t\treturn null;  // response;\n\t\t\t}\n\t\t} else {\n\t\t\tresponse.putAll(sqlResponse);\n\t\t}\n\n\n\t\t//把isTable时取出去的custom重新添加回来\n\t\tif (customMap != null) {\n\t\t\tresponse.putAll(customMap);\n\t\t}\n\n\n\t\tonFunctionResponse(\"0\");\n\n\t\tonChildResponse();\n\n\t\tonFunctionResponse(\"+\");\n\n\t\tonComplete();\n\n\t\treturn response;\n\t}\n\n\n\t@Override\n\tpublic void onFunctionResponse(String type) throws Exception {\n\t\tMap<String, String> map = functionMap == null ? null : functionMap.get(type);\n\n\t\t//解析函数function\n\t\tSet<Entry<String, String>> functionSet = map == null ? null : map.entrySet();\n\t\tif (functionSet != null && functionSet.isEmpty() == false) {\n            boolean isMinus = \"-\".equals(type);\n\t\t\tM json = isMinus ? sqlRequest : response; // key-():function 是实时执行，而不是在这里批量执行\n\n\t\t\tfor (Entry<String, String> entry : functionSet) {\n                parseFunction(entry.getKey(), entry.getKey(), entry.getValue(), this.type == TYPE_ITEM ? path : parentPath, name, json, isMinus);\n\t\t\t}\n\t\t}\n\t}\n\n\n\t//public void parseFunction(String key, String value, String parentPath, String currentName, JSONRequest currentObject) throws Exception {\n    //    parseFunction(key, value, parentPath, currentName, currentObject, false);\n    //}\n\tpublic void parseFunction(String rawKey, String key, String value, String parentPath\n            , String currentName, M currentObject, boolean isMinus) throws Exception {\n\t\tObject result;\n        boolean containRaw = rawKeyList != null && rawKeyList.contains(rawKey);\n\n        boolean isProcedure = key.startsWith(\"@\");\n\t\tif (isProcedure) {\n\t\t\tFunctionBean fb = AbstractFunctionParser.parseFunction(value, currentObject, true, containRaw);\n\n\t\t\tSQLConfig<T, M, L> config = newSQLConfig(true);\n            String sch = fb.getSchema();\n            if (StringUtil.isNotEmpty(sch, true)) {\n                config.setSchema(sch);\n            }\n\t\t\tconfig.setProcedure(fb.toFunctionCallString(true));\n\t\t\tresult = parseResponse(config, true);\n\n\t\t\tkey = key.substring(1);\n\t\t}\n\t\telse {\n\t\t\tresult = parser.onFunctionParse(key, value, parentPath, currentName, currentObject, containRaw);\n\t\t}\n\n\t\tString k = AbstractSQLConfig.gainRealKey(method, key, false, false);\n\n        if (isProcedure == false && isMinus) {\n            if (result != null) {\n                sqlRequest.put(k, result);\n            } else {\n                sqlRequest.remove(k);\n            }\n        }\n\n        if (result != null) {\n            response.put(k, result);\n        } else {\n            response.remove(k);\n        }\n\n        parser.putQueryResult(AbstractParser.getAbsPath(path, k), result);\n\t}\n\n\t@Override\n\tpublic void onChildResponse() throws Exception {\n\t\t//把isTable时取出去child解析后重新添加回来\n\t\tSet<Entry<String, M>> set = childMap == null ? null : childMap.entrySet();\n\t\tif (set != null) {\n\t\t\tint index = 0;\n\t\t\tfor (Entry<String, M> entry : set) {\n\t\t\t\tObject child = entry == null ? null : onChildParse(index, entry.getKey(), entry.getValue(), null);\n\t\t\t\tif (child == null\n\t\t\t\t\t\t|| (child instanceof Map<?, ?> && ((M) child).isEmpty())\n\t\t\t\t\t\t|| (child instanceof List<?> && ((L) child).isEmpty())\n\t\t\t\t\t\t) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tresponse.put(entry.getKey(), child);\n\t\t\t\tindex ++;\n\t\t\t}\n\t\t}\n\t}\n\n\n\n\t@Override\n\tpublic Object onReferenceParse(@NotNull String path) {\n\t\treturn parser.getValueByPath(path);\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic M onSQLExecute() throws Exception {\n\t\tint position = getPosition();\n\n\t\tM result = getCache();\n\t\tif (result != null) {\n\t\t\tparser.putQueryResult(path, result);\n\t\t}\n\t\telse if (isArrayMainTable && position > 0) {  // 数组主表使用专门的缓存数据\n\t\t\tresult = parser.getArrayMainCacheItem(parentPath.substring(0, parentPath.lastIndexOf(\"[]\") + 2), position);\n\t\t}\n\t\telse {\n\t\t\tresult = parser.executeSQL(sqlConfig, isSubquery);\n\n\t\t\tboolean isSimpleArray = false;\n            // 提取并缓存数组主表的列表数据\n            List<M> rawList = result == null ? null : (List<M>) result.remove(AbstractSQLExecutor.KEY_RAW_LIST);\n\n            if (isArrayMainTable && position == 0 && rawList != null) {\n\n                isSimpleArray = (functionMap == null || functionMap.isEmpty())\n                        && (customMap == null || customMap.isEmpty())\n                        && (childMap == null || childMap.isEmpty())\n                        && (table.equals(arrayTable));\n\n                // APP JOIN 副表时副表返回了这个字段   rawList = (List<JSONRequest>) result.remove(AbstractSQLExecutor.KEY_RAW_LIST);\n                String arrayPath = parentPath.substring(0, parentPath.lastIndexOf(\"[]\") + 2);\n\n                if (isSimpleArray) {\n\t\t\t\t\tparser.putQueryResult(arrayPath, rawList); // 从数组外部引用该数组内值需要\n\t\t\t\t} else {\n                    long startTime = System.currentTimeMillis();\n\n                    for (int i = 1; i < rawList.size(); i++) {  // 从 1 开始，0 已经处理过\n                        M obj = rawList.get(i);\n\n                        if (obj != null) {\n\t\t\t\t\t\t\t// obj.remove(AbstractSQLExecutor.KEY_VICE_ITEM);\n\t\t\t\t\t\t\tparser.putQueryResult(arrayPath + \"/\" + i + \"/\" + name, obj);  // 解决获取关联数据时requestObject里不存在需要的关联数据\n                        }\n                    }\n\n                    long endTime = System.currentTimeMillis();  // 3ms - 8ms\n                    Log.e(TAG, \"\\n onSQLExecute <<<<<<<<<<<<<<<<<<<<<<<<<<<<\"\n                            + \"\\n for (int i = 1; i < list.size(); i++)  startTime = \" + startTime\n                            + \"; endTime = \" + endTime + \"; duration = \" + (endTime - startTime)\n                            + \"\\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n \");\n                }\n\n                parser.putArrayMainCache(arrayPath, rawList);\n            }\n\n            if (isSubquery == false && result != null) {\n                parser.putQueryResult(path, result);  // 解决获取关联数据时requestObject里不存在需要的关联数据\n\n                if (isSimpleArray) { // FIXME 改为从缓存获取，而不是 result 查\n                    result.put(AbstractSQLExecutor.KEY_RAW_LIST, rawList);\n                }\n            }\n        }\n\n\t\treturn result;\n\t}\n\n\n\t/**\n\t * response has the final value after parse (and query if isTable)\n\t */\n\t@Override\n\tpublic void onComplete() {\n\t}\n\n\n\t/**回收内存\n\t */\n\t@Override\n\tpublic void recycle() {\n\t\t//后面还可能用到，要还原\n\t\tif (tri) {//避免返回未传的字段\n\t\t\trequest.put(KEY_TRY, tri);\n\t\t}\n\t\tif (drop) {\n\t\t\trequest.put(KEY_DROP, drop);\n\t\t}\n\t\tif (stringKeyList != null) { // 避免被全局关键词覆盖 && ! stringKeyList.isEmpty()) {\n\t\t\trequest.put(KEY_STRING, StringUtil.get(stringKeyList.toArray()));\n\t\t}\n\t\tif (trimKeyList != null) { // 避免被全局关键词覆盖 && ! trimKeyList.isEmpty()) {\n\t\t\trequest.put(KEY_TRIM, StringUtil.get(trimKeyList.toArray()));\n\t\t}\n\n\t\tmethod = null;\n\t\tparentPath = null;\n\t\tarrayConfig = null;\n\n\t\t//\t\tif (response != null) {\n\t\t//\t\t\tresponse.clear();//有效果?\n\t\t//\t\t\tresponse = null;\n\t\t//\t\t}\n\n\t\trequest = null;\n\t\tresponse = null;\n\t\tsqlRequest = null;\n\t\tsqlResponse = null;\n\n\t\tfunctionMap = null;\n\t\tcustomMap = null;\n\t\tchildMap = null;\n\t}\n\n\n\n\tprotected RequestMethod method;\n\t@Override\n\tpublic AbstractObjectParser<T, M, L> setMethod(RequestMethod method) {\n\t\tif (this.method != method) {\n\t\t\tthis.method = method;\n\t\t\tsqlConfig = null;\n\t\t\t//TODO ?\t\t\tsqlResponse = null;\n\t\t}\n\t\treturn this;\n\t}\n\t@Override\n\tpublic RequestMethod getMethod() {\n\t\treturn method;\n\t}\n\n\n\t@Override\n\tpublic boolean isTable() {\n\t\treturn isTable;\n\t}\n\t@Override\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\t@Override\n\tpublic String getTable() {\n\t\treturn table;\n\t}\n\t@Override\n\tpublic String getAlias() {\n\t\treturn alias;\n\t}\n\n\t@Override\n\tpublic SQLConfig<T, M, L> getArrayConfig() {\n\t\treturn arrayConfig;\n\t}\n\n\t@Override\n\tpublic SQLConfig<T, M, L> getSQLConfig() {\n\t\treturn sqlConfig;\n\t}\n\n\t@Override\n\tpublic M getResponse() {\n\t\treturn response;\n\t}\n\t@Override\n\tpublic M getSQLRequest() {\n\t\treturn sqlRequest;\n\t}\n\t@Override\n\tpublic M getSQLResponse() {\n\t\treturn sqlResponse;\n\t}\n\n\t@Override\n\tpublic Map<String, Object> getCustomMap() {\n\t\treturn customMap;\n\t}\n\t@Override\n\tpublic Map<String, Map<String, String>> getFunctionMap() {\n\t\treturn functionMap;\n\t}\n\t@Override\n\tpublic Map<String, M> getChildMap() {\n\t\treturn childMap;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.*;\nimport apijson.orm.exception.ConflictException;\n\nimport java.io.UnsupportedEncodingException;\nimport java.lang.management.ManagementFactory;\nimport java.net.InetAddress;\nimport java.net.URLDecoder;\nimport java.net.URLEncoder;\nimport java.sql.Connection;\nimport java.sql.SQLException;\nimport java.sql.Savepoint;\nimport java.util.*;\nimport java.util.Map.Entry;\n\nimport javax.management.MBeanServer;\nimport javax.management.ObjectName;\nimport javax.management.Query;\n\n\nimport apijson.orm.exception.CommonException;\nimport apijson.orm.exception.UnsupportedDataTypeException;\n\nimport static apijson.JSON.*;\nimport static apijson.JSONMap.*;\nimport static apijson.JSONRequest.*;\nimport static apijson.RequestMethod.CRUD;\nimport static apijson.RequestMethod.GET;\n\n/**Parser<T, M, L> for parsing request to JSONRequest\n * @author Lemon\n */\npublic abstract class AbstractParser<T, M extends Map<String, Object>, L extends List<Object>>\n\t\timplements Parser<T, M, L> {\n\tprotected static final String TAG = \"AbstractParser\";\n\n\t/**\n\t * JSON 对象、数组对应的数据源、版本、角色、method等\n\t */\n\tprotected Map<Object, Map<String, Object>> keyObjectAttributesMap = new HashMap<>();\n\t/**\n\t * 可以通过切换该变量来控制是否打印关键的接口请求内容。保守起见，该值默认为false。\n\t * 与 {@link Log#DEBUG} 任何一个为 true 都会打印关键的接口请求内容。\n\t */\n\tpublic static boolean IS_PRINT_REQUEST_STRING_LOG = false;\n\n\t/**\n\t * 打印大数据量日志的标识。线上环境比较敏感，可以通过切换该变量来控制异常栈抛出、错误日志打印。保守起见，该值默认为false。\n\t * 与 {@link Log#DEBUG} 任何一个为 true 都会打印关键的接口请求及响应信息。\n\t */\n\tpublic static boolean IS_PRINT_BIG_LOG = false;\n\n\t/**\n\t * 可以通过切换该变量来控制是否打印关键的接口请求结束时间。保守起见，该值默认为false。\n\t * 与 {@link Log#DEBUG} 任何一个为 true 都会打印关键的接口请求结束时间。\n\t */\n\tpublic static boolean IS_PRINT_REQUEST_ENDTIME_LOG = false;\n\n\t/**\n\t * 可以通过切换该变量来控制返回 trace:stack 字段，如果是 gson 则不设置为 false，避免序列化报错。\n\t * 与 {@link Log#DEBUG} 任何一个为 true 返回 trace:stack 字段。\n\t */\n\tpublic static boolean IS_RETURN_STACK_TRACE = true;\n\n\n\t/**\n\t * 分页页码是否从 1 开始，默认为从 0 开始\n\t */\n\tpublic static boolean IS_START_FROM_1 = false;\n\tpublic static int MAX_QUERY_PAGE = 100;\n\tpublic static int DEFAULT_QUERY_COUNT = 10;\n\tpublic static int MAX_QUERY_COUNT = 100;\n\tpublic static int MAX_UPDATE_COUNT = 10;\n\tpublic static int MAX_SQL_COUNT = 200;\n\tpublic static int MAX_OBJECT_COUNT = 5;\n\tpublic static int MAX_ARRAY_COUNT = 5;\n\tpublic static int MAX_QUERY_DEPTH = 5;\n\n\tpublic boolean isStartFrom1() {\n\t\treturn IS_START_FROM_1;\n\t}\n\t@Override\n\tpublic int getMinQueryPage() {\n\t\treturn isStartFrom1() ? 1 : 0;\n\t}\n\t@Override\n\tpublic int getMaxQueryPage() {\n\t\treturn MAX_QUERY_PAGE;\n\t}\n\t@Override\n\tpublic int getDefaultQueryCount() {\n\t\treturn DEFAULT_QUERY_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxQueryCount() {\n\t\treturn MAX_QUERY_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxUpdateCount() {\n\t\treturn MAX_UPDATE_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxSQLCount() {\n\t\treturn MAX_SQL_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxObjectCount() {\n\t\treturn MAX_OBJECT_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxArrayCount() {\n\t\treturn MAX_ARRAY_COUNT;\n\t}\n\t@Override\n\tpublic int getMaxQueryDepth() {\n\t\treturn MAX_QUERY_DEPTH;\n\t}\n\n\t/**\n\t * method = null\n\t */\n\tpublic AbstractParser() {\n\t\tthis(null);\n\t}\n\t/**needVerify = true\n\t * @param method null ? requestMethod = GET\n\t */\n\tpublic AbstractParser(RequestMethod method) {\n\t\tsuper();\n\t\tsetMethod(method);\n\t\tsetNeedVerifyRole(AbstractVerifier.ENABLE_VERIFY_ROLE);\n\t\tsetNeedVerifyContent(AbstractVerifier.ENABLE_VERIFY_CONTENT);\n\t}\n\t/**\n\t * @param method null ? requestMethod = GET\n\t * @param needVerify 仅限于为服务端提供方法免验证特权，普通请求不要设置为 false ！ 如果对应Table有权限也建议用默认值 true，保持和客户端权限一致\n\t */\n\tpublic AbstractParser(RequestMethod method, boolean needVerify) {\n\t\tsuper();\n\t\tsetMethod(method);\n\t\tsetNeedVerify(needVerify);\n\t}\n\n\tprotected boolean isRoot = true;\n\tpublic boolean isRoot() {\n\t\treturn isRoot;\n\t}\n\tpublic AbstractParser<T, M, L> setRoot(boolean isRoot) {\n\t\tthis.isRoot = isRoot;\n\t\treturn this;\n\t}\n\n\tpublic static final String KEY_REF = \"Reference\";\n\n\t/**警告信息\n\t * Map<\"Reference\", \"引用赋值获取路径 /Comment/userId 对应的值为 null！\">\n\t */\n\tprotected Map<String, String> warnMap = new LinkedHashMap<>();\n\tpublic String getWarn(String type) {\n\t\treturn warnMap == null ? null : warnMap.get(type);\n\t}\n\tpublic AbstractParser<T, M, L> putWarnIfNeed(String type, String warn) {\n\t\tif (Log.DEBUG) {\n\t\t\tString w = getWarn(type);\n\t\t\tif (StringUtil.isEmpty(w, true)) {\n\t\t\t\tputWarn(type, warn);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\tpublic AbstractParser<T, M, L> putWarn(String type, String warn) {\n\t\tif (warnMap == null) {\n\t\t\twarnMap = new LinkedHashMap<>();\n\t\t}\n\t\twarnMap.put(type, warn);\n\t\treturn this;\n\t}\n\t/**获取警告信息\n\t * @return\n\t */\n\tpublic String getWarnString() {\n\t\tSet<Entry<String, String>> set = warnMap == null ? null : warnMap.entrySet();\n\t\tif (set == null || set.isEmpty()) {\n\t\t\treturn null;\n\t\t}\n\n\t\tStringBuilder sb = new StringBuilder();\n\t\tfor (Entry<String, String> e : set) {\n\t\t\tString k = e == null ? null : e.getKey();\n\t\t\tString v = k == null ? null : e.getValue();\n\t\t\tif (StringUtil.isEmpty(v, true)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (StringUtil.isNotEmpty(k, true)) {\n\t\t\t\tsb.append(\"[\" + k + \"]: \");\n\t\t\t}\n\t\t\tsb.append(v + \"; \");\n\t\t}\n\n\t\treturn sb.toString();\n\t}\n\n\n\t@NotNull\n\tprotected Visitor<T> visitor;\n\t@NotNull\n\t@Override\n\tpublic Visitor<T> getVisitor() {\n\t\tif (visitor == null) {\n\t\t\tvisitor = new Visitor<T>() {\n\n\t\t\t\t@Override\n\t\t\t\tpublic T getId() {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t@Override\n\t\t\t\tpublic List<T> getContactIdList() {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\treturn visitor;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setVisitor(@NotNull Visitor<T> visitor) {\n\t\tthis.visitor = visitor;\n\t\treturn this;\n\t}\n\n\tprotected RequestMethod requestMethod;\n\t@NotNull\n\t@Override\n\tpublic RequestMethod getMethod() {\n\t\treturn requestMethod;\n\t}\n\t@NotNull\n\t@Override\n\tpublic AbstractParser<T, M, L> setMethod(RequestMethod method) {\n\t\tthis.requestMethod = method == null ? GET : method;\n\t\tthis.transactionIsolation = RequestMethod.isQueryMethod(method) ? Connection.TRANSACTION_NONE : Connection.TRANSACTION_REPEATABLE_READ;\n\t\treturn this;\n\t}\n\n\tprotected int version;\n\t@Override\n\tpublic int getVersion() {\n\t\treturn version;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setVersion(int version) {\n\t\tthis.version = version;\n\t\treturn this;\n\t}\n\n\tprotected String tag;\n\t@Override\n\tpublic String getTag() {\n\t\treturn tag;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setTag(String tag) {\n\t\tthis.tag = tag;\n\t\treturn this;\n\t}\n\n\tprotected String requestURL;\n\tpublic String getRequestURL() {\n\t\treturn requestURL;\n\t}\n\tpublic AbstractParser<T, M, L> setRequestURL(String requestURL) {\n\t\tthis.requestURL = requestURL;\n\t\treturn this;\n\t}\n\n\tprotected M requestObject;\n\t@Override\n\tpublic M getRequest() {\n\t\treturn requestObject;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setRequest(M request) {\n\t\tthis.requestObject = request;\n\t\treturn this;\n\t}\n\n\tprotected Boolean globalFormat;\n\tpublic AbstractParser<T, M, L> setGlobalFormat(Boolean globalFormat) {\n\t\tthis.globalFormat = globalFormat;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic Boolean getGlobalFormat() {\n\t\treturn globalFormat;\n\t}\n\tprotected String globalRole;\n\tpublic AbstractParser<T, M, L> setGlobalRole(String globalRole) {\n\t\tthis.globalRole = globalRole;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalRole() {\n\t\treturn globalRole;\n\t}\n\tprotected String globalDatabase;\n\tpublic AbstractParser<T, M, L> setGlobalDatabase(String globalDatabase) {\n\t\tthis.globalDatabase = globalDatabase;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalDatabase() {\n\t\treturn globalDatabase;\n\t}\n\n\tprotected String globalDatasource;\n\t@Override\n\tpublic String getGlobalDatasource() {\n\t\treturn globalDatasource;\n\t}\n\tpublic AbstractParser<T, M, L> setGlobalDatasource(String globalDatasource) {\n\t\tthis.globalDatasource = globalDatasource;\n\t\treturn this;\n\t}\n\n\tprotected String globalNamespace;\n\tpublic AbstractParser<T, M, L> setGlobalNamespace(String globalNamespace) {\n\t\tthis.globalNamespace = globalNamespace;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalNamespace() {\n\t\treturn globalNamespace;\n\t}\n\n\tprotected String globalCatalog;\n\tpublic AbstractParser<T, M, L> setGlobalCatalog(String globalCatalog) {\n\t\tthis.globalCatalog = globalCatalog;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalCatalog() {\n\t\treturn globalCatalog;\n\t}\n\n\tprotected String globalSchema;\n\tpublic AbstractParser<T, M, L> setGlobalSchema(String globalSchema) {\n\t\tthis.globalSchema = globalSchema;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalSchema() {\n\t\treturn globalSchema;\n\t}\n\n\tprotected Boolean globalExplain;\n\tpublic AbstractParser<T, M, L> setGlobalExplain(Boolean globalExplain) {\n\t\tthis.globalExplain = globalExplain;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic Boolean getGlobalExplain() {\n\t\treturn globalExplain;\n\t}\n\tprotected String globalCache;\n\tpublic AbstractParser<T, M, L> setGlobalCache(String globalCache) {\n\t\tthis.globalCache = globalCache;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic String getGlobalCache() {\n\t\treturn globalCache;\n\t}\n\n\t@Override\n\tpublic AbstractParser<T, M, L> setNeedVerify(boolean needVerify) {\n\t\tsetNeedVerifyLogin(needVerify);\n\t\tsetNeedVerifyRole(needVerify);\n\t\tsetNeedVerifyContent(needVerify);\n\t\treturn this;\n\t}\n\n\tprotected boolean needVerifyLogin;\n\t@Override\n\tpublic boolean isNeedVerifyLogin() {\n\t\treturn needVerifyLogin;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setNeedVerifyLogin(boolean needVerifyLogin) {\n\t\tthis.needVerifyLogin = needVerifyLogin;\n\t\treturn this;\n\t}\n\tprotected boolean needVerifyRole;\n\t@Override\n\tpublic boolean isNeedVerifyRole() {\n\t\treturn needVerifyRole;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setNeedVerifyRole(boolean needVerifyRole) {\n\t\tthis.needVerifyRole = needVerifyRole;\n\t\treturn this;\n\t}\n\tprotected boolean needVerifyContent;\n\t@Override\n\tpublic boolean isNeedVerifyContent() {\n\t\treturn needVerifyContent;\n\t}\n\t@Override\n\tpublic AbstractParser<T, M, L> setNeedVerifyContent(boolean needVerifyContent) {\n\t\tthis.needVerifyContent = needVerifyContent;\n\t\treturn this;\n\t}\n\n\tprotected SQLExecutor<T, M, L> sqlExecutor;\n\tprotected Verifier<T, M, L> verifier;\n\tprotected Map<String, Object> queryResultMap;//path-result\n\n\t@Override\n\tpublic SQLExecutor<T, M, L> getSQLExecutor() {\n\t\tif (sqlExecutor == null) {\n\t\t\tsqlExecutor = createSQLExecutor();\n\t\t}\n\t\tsqlExecutor.setParser(this);\n\t\treturn sqlExecutor;\n\t}\n\t@Override\n\tpublic Verifier<T, M, L> getVerifier() {\n\t\tif (verifier == null) {\n\t\t\tverifier = createVerifier().setVisitor(getVisitor());\n\t\t}\n\t\tverifier.setParser(this);\n\t\treturn verifier;\n\t}\n\n\t/**解析请求JSONObject\n\t * @param request => URLDecoder.decode(request, UTF_8);\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <M extends Map<String, Object>> M parseRequest(String request) throws Exception {\n\t\ttry {\n\t\t\tM req = JSON.parseObject(request);\n\t\t\tObjects.requireNonNull(req);\n\t\t\treturn req;\n\t\t} catch (Throwable e) {\n\t\t\tthrow new UnsupportedEncodingException(\"JSON格式不合法！\" + e.getMessage() + \"! \" + request);\n\t\t}\n\t}\n\n\t/**解析请求json并获取对应结果\n\t * @param request\n\t * @return\n\t */\n\t@Override\n\tpublic String parse(String request) {\n\t\treturn JSON.toJSONString(parseResponse(request));\n\t}\n\t/**解析请求json并获取对应结果\n\t * @param request\n\t * @return\n\t */\n\t@NotNull\n\t@Override\n\tpublic String parse(M request) {\n\t\treturn JSON.toJSONString(parseResponse(request));\n\t}\n\n\t/**解析请求json并获取对应结果\n\t * @param request 先parseRequest中URLDecoder.decode(request, UTF_8);再parseResponse(getCorrectRequest(...))\n\t * @return parseResponse(requestObject);\n\t */\n\t@NotNull\n\t@Override\n\tpublic M parseResponse(String request) {\n\t\tLog.d(TAG, \"\\n\\n\\n\\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n\"\n\t\t\t\t+ requestMethod + \"/parseResponse  request = \\n\" + request + \"\\n\\n\");\n\n\t\ttry {\n\t\t\trequestObject = JSON.parseObject(request);\n\t\t\tif (requestObject == null) {\n\t\t\t\tthrow new UnsupportedEncodingException(\"JSON格式不合法！\");\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\treturn newErrorResult(e, isRoot);\n\t\t}\n\n\t\treturn parseResponse(requestObject);\n\t}\n\n\tprivate int queryDepth;\n\tprivate long executedSQLDuration;\n\n\t/**解析请求json并获取对应结果\n\t * @param request\n\t * @return requestObject\n\t */\n\t@NotNull\n\t@Override\n\tpublic M parseResponse(M request) {\n\t\tlong startTime = System.currentTimeMillis();\n\t\tLog.d(TAG, \"parseResponse  startTime = \" + startTime\n\t\t\t\t+ \"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n\\n\\n \");\n\n\t\trequestObject = request;\n\t\ttry {\n\t\t\tsetGlobalFormat(getBoolean(requestObject, KEY_FORMAT));\n\t\t\trequestObject.remove(KEY_FORMAT);\n\t\t} catch (Exception e) {\n\t\t\treturn extendErrorResult(requestObject, e, requestMethod, getRequestURL(), isRoot);\n\t\t}\n\n\t\ttry {\n\t\t\tsetVersion(getIntValue(requestObject, KEY_VERSION));\n\t\t\trequestObject.remove(KEY_VERSION);\n\n\t\t\tif (getMethod() != RequestMethod.CRUD) {\n\t\t\t\tsetTag(getString(requestObject, KEY_TAG));\n\t\t\t\trequestObject.remove(KEY_TAG);\n\t\t\t}\n\t\t} catch (Exception e) {\n\t\t\treturn extendErrorResult(requestObject, e, requestMethod, getRequestURL(), isRoot);\n\t\t}\n\n\t\tverifier = createVerifier().setVisitor(getVisitor());\n\n\t\tif (RequestMethod.isPublicMethod(requestMethod) == false) {\n\t\t\ttry {\n\t\t\t\tif (isNeedVerifyLogin()) {\n\t\t\t\t\tonVerifyLogin();\n\t\t\t\t}\n\t\t\t\tif (isNeedVerifyContent()) {\n\t\t\t\t\tonVerifyContent();\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\treturn extendErrorResult(requestObject, e, requestMethod, getRequestURL(), isRoot);\n\t\t\t}\n\t\t}\n\n\t\t//必须在parseCorrectRequest后面，因为parseCorrectRequest可能会添加 @role\n\t\tif (isNeedVerifyRole() && globalRole == null) {\n\t\t\ttry {\n\t\t\t\tsetGlobalRole(getString(requestObject, KEY_ROLE));\n\t\t\t\trequestObject.remove(KEY_ROLE);\n\t\t\t} catch (Exception e) {\n\t\t\t\treturn extendErrorResult(requestObject, e, requestMethod, getRequestURL(), isRoot);\n\t\t\t}\n\t\t}\n\n\t\ttry {\n\t\t\tsetGlobalDatabase(getString(requestObject, KEY_DATABASE));\n\t\t\tsetGlobalDatasource(getString(requestObject, KEY_DATASOURCE));\n\t\t\tsetGlobalNamespace(getString(requestObject, KEY_NAMESPACE));\n\t\t\tsetGlobalCatalog(getString(requestObject, KEY_CATALOG));\n\t\t\tsetGlobalSchema(getString(requestObject, KEY_SCHEMA));\n\n\t\t\tsetGlobalExplain(getBoolean(requestObject, KEY_EXPLAIN));\n\t\t\tsetGlobalCache(getString(requestObject, KEY_CACHE));\n\n\t\t\trequestObject.remove(KEY_DATABASE);\n\t\t\trequestObject.remove(KEY_DATASOURCE);\n\t\t\trequestObject.remove(KEY_NAMESPACE);\n\t\t\trequestObject.remove(KEY_CATALOG);\n\t\t\trequestObject.remove(KEY_SCHEMA);\n\n\t\t\trequestObject.remove(KEY_EXPLAIN);\n\t\t\trequestObject.remove(KEY_CACHE);\n\t\t} catch (Exception e) {\n\t\t\treturn extendErrorResult(requestObject, e, requestMethod, getRequestURL(), isRoot);\n\t\t}\n\n\t\tfinal String requestString = JSON.toJSONString(request);//request传进去解析后已经变了\n\n\t\tqueryResultMap = new HashMap<String, Object>();\n\n\t\tException error = null;\n\t\tsqlExecutor = getSQLExecutor();\n\t\tonBegin();\n\t\ttry {\n\t\t\tqueryDepth = 0;\n\t\t\texecutedSQLDuration = 0;\n\n\t\t\trequestObject = onObjectParse(request, null, null, null, false, null);\n\n\t\t\tonCommit();\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\tLog.e(TAG, \"onObjectParse failed\", e);\n\t\t\terror = e;\n\n\t\t\tonRollback();\n\t\t}\n\n\t\tString warn = Log.DEBUG == false || error != null ? null : getWarnString();\n\n\t\trequestObject = error == null ? extendSuccessResult(requestObject, warn, isRoot) : extendErrorResult(requestObject, error, requestMethod, getRequestURL(), isRoot);\n\n\t\t// FIXME 暂时先直接移除，后续排查是在哪里 put 进来\n\t\trequestObject.remove(KEY_DATABASE);\n\t\trequestObject.remove(KEY_DATASOURCE);\n\t\trequestObject.remove(KEY_NAMESPACE);\n\t\trequestObject.remove(KEY_CATALOG);\n\t\trequestObject.remove(KEY_SCHEMA);\n\n\t\tM res = (globalFormat != null && globalFormat) && JSONResponse.isSuccess(requestObject) ? JSONResponse.format(requestObject) : requestObject;\n\n\t\tlong endTime = System.currentTimeMillis();\n\t\tlong duration = endTime - startTime;\n\n\t\tres.putIfAbsent(\"time\", endTime);\n\t\tif (Log.DEBUG) {\n\t\t\tsqlExecutor = getSQLExecutor();\n\t\t\tres.put(\"sql:generate|cache|execute|maxExecute\", sqlExecutor.getGeneratedSQLCount() + \"|\" + sqlExecutor.getCachedSQLCount() + \"|\" + sqlExecutor.getExecutedSQLCount() + \"|\" + getMaxSQLCount());\n\t\t\tres.put(\"depth:count|max\", queryDepth + \"|\" + getMaxQueryDepth());\n\n\t\t\texecutedSQLDuration += sqlExecutor.getExecutedSQLDuration() + sqlExecutor.getSqlResultDuration();\n\t\t\tlong parseDuration = duration - executedSQLDuration;\n\t\t\tres.put(\"time:start|duration|end|parse|sql\", startTime + \"|\" + duration + \"|\" + endTime + \"|\" + parseDuration + \"|\" + executedSQLDuration);\n\n\t\t\tif (error != null) {\n                //        String msg = error.getMessage();\n                //        if (msg != null && msg.contains(Log.KEY_SYSTEM_INFO_DIVIDER)) {\n                //        }\n                Throwable t = error instanceof CommonException && error.getCause() != null ? error.getCause() : error;\n\t\t\t\tres.put(\"trace:throw\", t.getClass().getName());\n\n\t\t\t\tif (IS_RETURN_STACK_TRACE) {\n\t\t\t\t\tL list = JSON.createJSONArray();\n\n\t\t\t\t\tStackTraceElement[] traces = t.getStackTrace();\n\t\t\t\t\tif (traces != null) { // && traces.length > 0) {\n\t\t\t\t\t\tfor (StackTraceElement trace : traces) {\n\t\t\t\t\t\t\tlist.add(trace == null ? null : trace.toString());\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tres.put(\"trace:stack\", list);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tonClose();\n\n\t\t// CS304 Issue link: https://github.com/Tencent/APIJSON/issues/232\n\t\tif (IS_PRINT_REQUEST_STRING_LOG || Log.DEBUG || error != null) {\n\t\t\tLog.sl(\"\\n\\n\\n\", '<', \"\");\n\t\t\tLog.fd(TAG, requestMethod + \"/parseResponse  request = \\n\" + requestString + \"\\n\\n\");\n\t\t}\n\t\tif (IS_PRINT_BIG_LOG || Log.DEBUG || error != null) {  // 日志仅存服务器，所以不太敏感，而且这些日志虽然量大但非常重要，对排查 bug 很关键\n\t\t\tLog.fd(TAG, requestMethod + \"/parseResponse return response = \\n\" + JSON.toJSONString(requestObject) + \"\\n\\n\");\n\t\t}\n\t\tif (IS_PRINT_REQUEST_ENDTIME_LOG || Log.DEBUG || error != null) {\n\t\t\tLog.fd(TAG, requestMethod + \"/parseResponse  endTime = \" + endTime + \";  duration = \" + duration);\n\t\t\tLog.sl(\"\", '>', \"\\n\\n\\n\");\n\t\t}\n\n\t\treturn res;\n\t}\n\n\n\t@Override\n\tpublic void onVerifyLogin() throws Exception {\n\t\tgetVerifier().verifyLogin();\n\t}\n\t@Override\n\tpublic void onVerifyContent() throws Exception {\n\t\trequestObject = parseCorrectRequest();\n\t}\n\t/**校验角色及对应操作的权限\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic void onVerifyRole(@NotNull SQLConfig<T, M, L> config) throws Exception {\n\t\tif (Log.DEBUG) {\n\t\t\tLog.i(TAG, \"onVerifyRole  config = \" + JSON.toJSONString(config));\n\t\t}\n\n\t\tif (isNeedVerifyRole()) {\n\t\t\tif (config.getRole() == null) {\n\t\t\t\tif (globalRole != null) {\n\t\t\t\t\tconfig.setRole(globalRole);\n\t\t\t\t} else {\n\t\t\t\t\tconfig.setRole(getVisitor().getId() == null ? AbstractVerifier.UNKNOWN : AbstractVerifier.LOGIN);\n\t\t\t\t}\n\t\t\t}\n\t\t\tgetVerifier().verifyAccess(config);\n\t\t}\n\n\t}\n\n\n\t@Override\n\tpublic M parseCorrectRequest(RequestMethod method, String tag, int version, String name, @NotNull M request\n\t\t\t, int maxUpdateCount, SQLCreator<T, M, L> creator) throws Exception {\n\n\t\tif (RequestMethod.isPublicMethod(method)) {\n\t\t\treturn request;//需要指定JSON结构的get请求可以改为post请求。一般只有对安全性要求高的才会指定，而这种情况用明文的GET方式几乎肯定不安全\n\t\t}\n\n\t\treturn batchVerify(method, tag, version, name, request, maxUpdateCount, creator);\n\t}\n\n\t/**自动根据 tag 是否为 TableKey 及是否被包含在 object 内来决定是否包装一层，改为 { tag: object, \"tag\": tag }\n\t * @param object\n\t * @param tag\n\t * @return\n\t */\n\tpublic M wrapRequest(RequestMethod method, String tag, M object, boolean isStructure) {\n\t\tboolean putTag = ! isStructure;\n\n\t\tif (object == null || object.containsKey(tag)) { //tag 是 Table 名或 Table[]\n\t\t\tif (putTag) {\n\t\t\t\tif (object == null) {\n\t\t\t\t\tobject = JSON.createJSONObject();\n\t\t\t\t}\n\t\t\t\tobject.put(KEY_TAG, tag);\n\t\t\t}\n\t\t\treturn object;\n\t\t}\n\n\t\tboolean isDiffArrayKey = tag.endsWith(\":[]\");\n\t\tboolean isArrayKey = isDiffArrayKey || isArrayKey(tag);\n\t\tString key = isArrayKey ? tag.substring(0, tag.length() - (isDiffArrayKey ? 3 : 2)) : tag;\n\n\t\tM target = object;\n\t\tif (isTableKey(key)) {\n\t\t\tif (isDiffArrayKey) { //自动为 tag = Comment:[] 的 { ... } 新增键值对为 { \"Comment[]\":[], \"TYPE\": { \"Comment[]\": \"OBJECT[]\" } ... }\n\t\t\t\tif (isStructure && (method == RequestMethod.POST || method == RequestMethod.PUT)) {\n\t\t\t\t\tString arrKey = key + \"[]\";\n\n\t\t\t\t\tif (target.containsKey(arrKey) == false) {\n\t\t\t\t\t\ttarget.put(arrKey, JSON.createJSONArray());\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tMap<String, Object> type = JSON.get(target, Operation.TYPE.name());\n\t\t\t\t\t\tif (type == null || (type.containsKey(arrKey) == false)) {\n\t\t\t\t\t\t\tif (type == null) {\n\t\t\t\t\t\t\t\ttype = new LinkedHashMap<String, Object>();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttype.put(arrKey, \"OBJECT[]\");\n\t\t\t\t\t\t\ttarget.put(Operation.TYPE.name(), type);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcatch (Throwable e) {\n\t\t\t\t\t\tLog.w(TAG, \"wrapRequest try { Map<String, Object> type = target.getJSONObject(Operation.TYPE.name()); } catch (Exception e) = \" + e.getMessage());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse { //自动为 tag = Comment 的 { ... } 包一层为 { \"Comment\": { ... } }\n\t\t\t\tif (isArrayKey == false || RequestMethod.isGetMethod(method, true)) {\n\t\t\t\t\ttarget = JSON.createJSONObject();\n\t\t\t\t\ttarget.put(tag, object);\n\t\t\t\t}\n\t\t\t\telse if (target.containsKey(key) == false) {\n\t\t\t\t\ttarget = JSON.createJSONObject();\n\t\t\t\t\ttarget.put(key, object);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (putTag) {\n\t\t\ttarget.put(KEY_TAG, tag);\n\t\t}\n\n\t\treturn target;\n\t}\n\n\n\t/**新建带状态内容的JSONObject\n\t * @param code\n\t * @param msg\n\t * @return\n\t */\n\tpublic M newResult(int code, String msg) {\n\t\treturn newResult(code, msg, null);\n\t}\n\n\t/**\n\t * 添加JSONObject的状态内容，一般用于错误提示结果\n\t *\n\t * @param code\n\t * @param msg\n\t * @param warn\n\t * @return\n\t */\n\tpublic M newResult(int code, String msg, String warn) {\n\t\treturn newResult(code, msg, warn, false);\n\t}\n\n\t/**\n\t * 新建带状态内容的JSONObject\n\t *\n\t * @param code\n\t * @param msg\n\t * @param warn\n\t * @param isRoot\n\t * @return\n\t */\n\tpublic M newResult(int code, String msg, String warn, boolean isRoot) {\n\t\treturn extendResult(null, code, msg, warn, isRoot);\n\t}\n\n\t/**\n\t * 添加JSONObject的状态内容，一般用于错误提示结果\n\t *\n\t * @param object\n\t * @param code\n\t * @param msg\n\t * @return\n\t */\n\tpublic M extendResult(M object, int code, String msg, String warn, boolean isRoot) {\n\t\tint index = Log.DEBUG == false || isRoot == false || msg == null ? -1 : msg.lastIndexOf(Log.KEY_SYSTEM_INFO_DIVIDER);\n\t\tString debug = Log.DEBUG == false || isRoot == false ? null : (index >= 0 ? msg.substring(index + Log.KEY_SYSTEM_INFO_DIVIDER.length()).trim()\n\t\t\t\t: \" \\n提 bug 请发请求和响应的【完整截屏】，没图的自行解决！\"\n\t\t\t\t+ \" \\n开发者有限的时间和精力主要放在【维护项目源码和文档】上！\"\n\t\t\t\t+ \" \\n【描述不详细】 或 【文档/常见问题 已有答案】 的问题可能会被忽略！！\"\n\t\t\t\t+ \" \\n【态度 不文明/不友善】的可能会被踢出群，问题也可能不予解答！！！\"\n\t\t\t\t+ \" \\n\\n **环境信息** \"\n\t\t\t\t+ \" \\n系统: \" + Log.OS_NAME + \" \" + Log.OS_VERSION\n\t\t\t\t+ \" \\n数据库: DEFAULT_DATABASE = \" + AbstractSQLConfig.DEFAULT_DATABASE\n\t\t\t\t+ \" \\nJDK: \" + Log.JAVA_VERSION + \" \" + Log.OS_ARCH\n\t\t\t\t+ \" \\nAPIJSON: \" + Log.VERSION\n\t\t\t\t+ \" \\n   \\n【常见问题】：https://github.com/Tencent/APIJSON/issues/36\"\n\t\t\t\t+ \" \\n【通用文档】：https://github.com/Tencent/APIJSON/blob/master/Document.md\"\n\t\t\t\t+ \" \\n【视频教程】：https://search.bilibili.com/all?keyword=APIJSON\");\n\n\t\tmsg = index >= 0 ? msg.substring(0, index) : msg;\n\n\t\tif (object == null) {\n\t\t\tobject = JSON.createJSONObject();\n\t\t}\n\n\t\tif (object.get(JSONResponse.KEY_OK) == null) {\n\t\t\tobject.put(JSONResponse.KEY_OK, JSONResponse.isSuccess(code));\n\t\t}\n\t\tif (object.get(JSONResponse.KEY_CODE) == null) {\n\t\t\tobject.put(JSONResponse.KEY_CODE, code);\n\t\t}\n\n\t\tString m = StringUtil.get(getString(object, JSONResponse.KEY_MSG));\n\t\tif (m.isEmpty() == false) {\n\t\t\tmsg = m + \" ;\\n \" + StringUtil.get(msg);\n\t\t}\n\n\t\tobject.put(JSONResponse.KEY_MSG, msg);\n\t\tif (debug != null) {\n\t\t\tif (StringUtil.isNotEmpty(warn, true)) {\n\t\t\t\tdebug += \"\\n 【警告】：\" + warn;\n\t\t\t}\n\t\t\tobject.put(\"debug:info|help\", debug);\n\t\t}\n\n\t\treturn object;\n\t}\n\n\n\t/**\n\t * 添加请求成功的状态内容\n\t *\n\t * @param object\n\t * @return\n\t */\n\tpublic M extendSuccessResult(M object) {\n\t\treturn extendSuccessResult(object, false);\n\t}\n\n\tpublic M extendSuccessResult(M object, boolean isRoot) {\n\t\treturn extendSuccessResult(object, null, isRoot);\n\t}\n\n\t/**添加请求成功的状态内容\n\t * @param object\n\t * @param isRoot\n\t * @return\n\t */\n\tpublic M extendSuccessResult(M object, String warn, boolean isRoot) {\n\t\treturn extendResult(object, JSONResponse.CODE_SUCCESS, JSONResponse.MSG_SUCCEED, warn, isRoot);\n\t}\n\n\t/**获取请求成功的状态内容\n\t * @return\n\t */\n\tpublic M newSuccessResult() {\n\t\treturn newSuccessResult(null);\n\t}\n\n\t/**获取请求成功的状态内容\n\t * @param warn\n\t * @return\n\t */\n\tpublic M newSuccessResult(String warn) {\n\t\treturn newSuccessResult(warn, false);\n\t}\n\n\t/**获取请求成功的状态内容\n\t * @param warn\n\t * @param isRoot\n\t * @return\n\t */\n\tpublic M newSuccessResult(String warn, boolean isRoot) {\n\t\treturn newResult(JSONResponse.CODE_SUCCESS, JSONResponse.MSG_SUCCEED, warn, isRoot);\n\t}\n\n\t/**添加请求成功的状态内容\n\t * @param object\n\t * @param e\n\t * @return\n\t */\n\tpublic M extendErrorResult(M object, Throwable e) {\n\t\treturn extendErrorResult(object, e, false);\n\t}\n\t/**添加请求成功的状态内容\n\t * @param object\n\t * @param e\n\t * @param isRoot\n\t * @return\n\t */\n\tpublic M extendErrorResult(M object, Throwable e, boolean isRoot) {\n\t\treturn extendErrorResult(object, e, null, null, isRoot);\n\t}\n\t/**添加请求成功的状态内容\n\t * @param object\n\t * @return\n\t */\n\tpublic M extendErrorResult(M object, Throwable e, RequestMethod requestMethod, String url, boolean isRoot) {\n        String msg = CommonException.getMsg(e);\n\n        if (Log.DEBUG && isRoot) {\n            try {\n                boolean isCommon = e instanceof CommonException;\n                String env = isCommon ? ((CommonException) e).getEnvironment() : null;\n                if (StringUtil.isEmpty(env)) {\n                    //int index = msg.lastIndexOf(Log.KEY_SYSTEM_INFO_DIVIDER);\n                    //env = index >= 0 ? msg.substring(index + Log.KEY_SYSTEM_INFO_DIVIDER.length()).trim()\n                    env = \" \\n **环境信息** \"\n                            + \" \\n 系统: \" + Log.OS_NAME + \" \" + Log.OS_VERSION\n                            + \" \\n 数据库: <!-- 请填写，例如 MySQL 5.7。默认数据库为 \" + AbstractSQLConfig.DEFAULT_DATABASE + \" -->\"\n                            + \" \\n JDK: \" + Log.JAVA_VERSION + \" \" + Log.OS_ARCH\n                            + \" \\n APIJSON: \" + Log.VERSION;\n\n                    //msg = index < 0 ? msg : msg.substring(0, index).trim();\n                }\n\n                String encodedMsg = URLEncoder.encode(msg, \"UTF-8\");\n\n                if (StringUtil.isEmpty(url, true)) {\n                    String host = \"localhost\";\n                    try {\n                        host = InetAddress.getLocalHost().getHostAddress();\n                    } catch (Throwable e2) {}\n\n                    String port = \"8080\";\n                    try {\n                        MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();\n\n                        Set<ObjectName> objectNames = beanServer.queryNames(\n                                new ObjectName(\"*:type=Connector,*\"),\n                                Query.match(Query.attr(\"protocol\"), Query.value(\"HTTP/1.1\"))\n                        );\n                        String p = objectNames.iterator().next().getKeyProperty(\"port\");\n                        port = StringUtil.isEmpty(p, true) ? port : p;\n                    } catch (Throwable e2) {}\n\n                    url = \"http://\" + host + \":\" + port + \"/\" + (requestMethod == null ? RequestMethod.GET : requestMethod).name().toLowerCase();\n                }\n\n                String req = JSON.toJSONString(object);\n                try {\n                    req = URLEncoder.encode(req, \"UTF-8\");\n                } catch (Throwable e2) {}\n\n                Throwable t = isCommon ? e.getCause() : e;\n                boolean isSQLException = t instanceof SQLException;  // SQL 报错一般都是通用问题，优先搜索引擎\n                String apiatuoAndGitHubLink = \"\\n\\n【APIAuto】： \\n http://apijson.cn/api?type=JSON&url=\" + URLEncoder.encode(url, \"UTF-8\") + \"&json=\" + req\n                        + \"        \\n\\n【GitHub】： \\n https://www.google.com/search?q=site%3Agithub.com%2FTencent%2FAPIJSON+++\" + encodedMsg;\n\n                msg += Log.KEY_SYSTEM_INFO_DIVIDER + \"    浏览器打开以下链接查看解答\"\n                        + (isSQLException ? \"\" : apiatuoAndGitHubLink)\n                        //\tGitHub Issue 搜索貌似是精准包含，不易找到答案 \t+ \"        \\n\\nGitHub： \\n https://github.com/Tencent/APIJSON/issues?q=is%3Aissue+\" + encodedMsg\n                        + \"        \\n\\n【Google】：\\n https://www.google.com/search?q=\" + encodedMsg\n                        + \"        \\n\\n【百度】：\\n https://www.baidu.com/s?ie=UTF-8&wd=\" + encodedMsg\n                        + (isSQLException ? apiatuoAndGitHubLink : \"\")\n                        + \"        \\n\\n都没找到答案？打开这个链接 \\n https://github.com/Tencent/APIJSON/issues/new?assignees=&labels=&template=--bug.md  \"\n                        + \" \\n然后提交问题，推荐用以下模板修改，注意要换行保持清晰可读。\"\n                        + \" \\n【标题】：\" + msg\n                        + \" \\n【内容】：\" + env + \"\\n\\n**问题描述**\\n\" + msg\n                        + \" \\n\\n<!-- 尽量完整截屏(至少包含请求和回包结果，还可以加上控制台报错日志)，然后复制粘贴到这里 -->\"\n                        + \" \\n\\nPOST \" + url\n                        + \" \\n发送请求 Request JSON：\\n ```js\"\n                        + \" \\n 请填写，例如 { \\\"Users\\\":{} }\"\n                        + \" \\n```\"\n                        + \" \\n\\n返回结果 Response JSON：\\n ```js\"\n                        + \" \\n 请填写，例如 { \\\"Users\\\": {}, \\\"code\\\": 401, \\\"msg\\\": \\\"Users 不允许 UNKNOWN 用户的 GET 请求！\\\" }\"\n                        + \" \\n```\";\n            } catch (Throwable e2) {}\n        }\n\n        int code = CommonException.getCode(e);\n        return extendResult(object, code, msg, null, isRoot);\n    }\n\n\t/**新建错误状态内容\n\t * @param e\n\t * @return\n\t */\n\tpublic M newErrorResult(Exception e) {\n\t\treturn newErrorResult(e, false);\n\t}\n\t/**新建错误状态内容\n\t * @param e\n\t * @param isRoot\n\t * @return\n\t */\n\tpublic M newErrorResult(Exception e, boolean isRoot) {\n\t\tif (e != null) {\n\t\t  \tLog.e(TAG, \"newErrorResult\", e);\n\n\t\t  \tString msg = CommonException.getMsg(e);\n\t\t\tint code = CommonException.getCode(e);\n\n\t\t\treturn newResult(code, msg, null, isRoot);\n\t\t}\n\n\t\treturn newResult(JSONResponse.CODE_SERVER_ERROR, JSONResponse.MSG_SERVER_ERROR, null, isRoot);\n\t}\n\n\n\t/**获取正确的请求，非GET请求必须是服务器指定的\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic M parseCorrectRequest() throws Exception {\n\t\treturn parseCorrectRequest(requestMethod, tag, version, \"\", requestObject, getMaxUpdateCount(), this);\n\t}\n\n\n\t/**获取Request或Response内指定JSON结构\n\t * @param table\n\t * @param method\n\t * @param tag\n\t * @param version\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic M getStructure(@NotNull String table, String method, String tag, int version) throws Exception  {\n\t\tString cacheKey = AbstractVerifier.getCacheKeyForRequest(method, tag);\n\t\tSortedMap<Integer, Map<String, Object>> versionedMap = (SortedMap<Integer, Map<String, Object>>) AbstractVerifier.REQUEST_MAP.get(cacheKey);\n\n\t\tMap<String, Object> result = versionedMap == null ? null : versionedMap.get(Integer.valueOf(version));\n\t\tif (result == null) {  // version <= 0 时使用最新，version > 0 时使用 > version 的最接近版本（最小版本）\n\t\t\tSet<? extends Entry<Integer, ? extends Map<String, Object>>> set = versionedMap == null ? null : versionedMap.entrySet();\n\n\t\t\tif (set != null && set.isEmpty() == false) {\n\t\t\t\tEntry<Integer, ? extends Map<String, Object>> maxEntry = null;\n\n\t\t\t\tfor (Entry<Integer, ? extends Map<String, Object>> entry : set) {\n\t\t\t\t\tif (entry == null || entry.getKey() == null || entry.getValue() == null) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (version <= 0 || version == entry.getKey()) {  // 这里应该不会出现相等，因为上面 versionedMap.get(Integer.valueOf(version))\n\t\t\t\t\t\tmaxEntry = entry;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (entry.getKey() < version) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tmaxEntry = entry;\n\t\t\t\t}\n\n\t\t\t\tresult = maxEntry == null ? null : maxEntry.getValue();\n\t\t\t}\n\n\t\t\tif (result != null) {  // 加快下次查询，查到值的话组合情况其实是有限的，不属于恶意请求\n\t\t\t\tif (versionedMap == null) {\n\t\t\t\t\tversionedMap = new TreeMap<>((o1, o2) -> {\n\t\t\t\t\t\treturn o2 == null ? -1 : o2.compareTo(o1);  // 降序\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tversionedMap.put(Integer.valueOf(version), result);\n\t\t\t\tAbstractVerifier.REQUEST_MAP.put(cacheKey, versionedMap);\n\t\t\t}\n\t\t}\n\n\t\tif (result == null) {\n\t\t\tif (Log.DEBUG == false && AbstractVerifier.REQUEST_MAP.isEmpty() == false) {\n\t\t\t\treturn null;  // 已使用 REQUEST_MAP 缓存全部，但没查到\n\t\t\t}\n\n\t\t\t// 获取指定的JSON结构 <<<<<<<<<<<<<<\n\t\t\tSQLConfig<T, M, L> config = createSQLConfig().setMethod(GET).setTable(table);\n\t\t\tconfig.setParser(this);\n\t\t\tconfig.setPrepared(false);\n\t\t\tconfig.setColumn(Arrays.asList(\"structure\"));\n\n\t\t\tMap<String, Object> where = new HashMap<String, Object>();\n\t\t\twhere.put(\"method\", method);\n\t\t\twhere.put(KEY_TAG, tag);\n\n\t\t\tif (version > 0) {\n\t\t\t\twhere.put(KEY_VERSION + \">=\", version);\n\t\t\t}\n\t\t\tconfig.setWhere(where);\n\t\t\tconfig.setOrder(KEY_VERSION + (version > 0 ? \"+\" : \"-\"));\n\t\t\tconfig.setCount(1);\n\n\t\t\t// too many connections error: 不try-catch，可以让客户端看到是服务器内部异常\n\t\t\tresult = getSQLExecutor().execute(config, false);\n\n\t\t\t// version, method, tag 组合情况太多了，JDK 里又没有 LRUCache，所以要么启动时一次性缓存全部后面只用缓存，要么每次都查数据库\n\t\t\t//\t\t\tversionedMap.put(Integer.valueOf(version), result);\n\t\t\t//\t\t\tAbstractVerifier.REQUEST_MAP.put(cacheKey, versionedMap);\n\t\t}\n\n\t\treturn JSON.get(result, \"structure\"); //解决返回值套了一层 \"structure\":{}\n\t}\n\n\n\n\tprotected Map<String, ObjectParser<T, M, L>> arrayObjectParserCacheMap = new HashMap<>();\n\n\t//\tprotected SQLConfig<T, M, L> itemConfig;\n\t/**获取单个对象，该对象处于parentObject内\n\t * @param request parentObject 的 value\n\t * @param parentPath parentObject 的路径\n\t * @param name parentObject 的 key\n\t * @param arrayConfig config for array item\n\t * @param isSubquery 是否为子查询\n\t * @param cache SQL 结果缓存\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic M onObjectParse(final M request, String parentPath, String name\n\t\t\t, final SQLConfig<T, M, L> arrayConfig, boolean isSubquery, M cache) throws Exception {\n\n\t\tif (Log.DEBUG) {\n\t\t\tLog.i(TAG, \"\\ngetObject:  parentPath = \" + parentPath\n\t\t\t\t\t+ \";\\n name = \" + name + \"; request = \" + JSON.toJSONString(request));\n\t\t}\n\t\tif (request == null) {// Moment:{}   || request.isEmpty()) {//key-value条件\n\t\t\treturn null;\n\t\t}\n\n\t\tint type = arrayConfig == null ? 0 : arrayConfig.getType();\n\t\tint position = arrayConfig == null ? 0 : arrayConfig.getPosition();\n\n\t\tString[] arr = StringUtil.split(parentPath, \"/\");\n\t\tif (position == 0) {\n\t\t\tint d = arr == null ? 1 : arr.length + 1;\n\t\t\tif (queryDepth < d) {\n\t\t\t\tqueryDepth = d;\n\t\t\t\tint maxQueryDepth = getMaxQueryDepth();\n\t\t\t\tif (queryDepth > maxQueryDepth) {\n\t\t\t\t\tthrow new IllegalArgumentException(parentPath + \"/\" + name + \":{} 的深度(或者说层级) 为 \" + queryDepth + \" 已超限，必须在 1-\" + maxQueryDepth + \" 内 !\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tapijson.orm.Entry<String, String> entry = Pair.parseEntry(name, true);\n\t\tString table = entry.getKey(); //Comment\n\t\t// String alias = entry.getValue(); //to\n\n\t\tboolean isTable = isTableKey(table);\n\t\tboolean isArrayMainTable = isSubquery == false && isTable && type == SQLConfig.TYPE_ITEM_CHILD_0 && arrayConfig != null && RequestMethod.isGetMethod(arrayConfig.getMethod(), true);\n\t\tboolean isReuse = isArrayMainTable && position > 0;\n\n\t\tObjectParser<T, M, L> op = null;\n\t\tif (isReuse) {  // 数组主表使用专门的缓存数据\n\t\t\top = arrayObjectParserCacheMap.get(parentPath.substring(0, parentPath.lastIndexOf(\"[]\") + 2));\n\t\t\top.setParentPath(parentPath);\n\t\t}\n\n\t\tif (op == null) {\n\t\t\top = createObjectParser(request, parentPath, arrayConfig, isSubquery, isTable, isArrayMainTable);\n\t\t}\n\t\t// 对象 - 设置 method\n\t\tsetOpMethod(request, op, name);\n\n\t\top.setCache(cache);\n\t\top = op.parse(name, isReuse);\n\n\t\tM response = null;\n\t\tif (op != null) {//SQL查询结果为空时，functionMap和customMap没有意义\n\n\t\t\tif (arrayConfig == null) { //Common\n\t\t\t\tresponse = op.setSQLConfig().executeSQL().response();\n\t\t\t}\n\t\t\telse {//Array Item Child\n\t\t\t\tint query = arrayConfig.getQuery();\n\n\t\t\t\t//total 这里不能用arrayConfig.getType()，因为在createObjectParser.onChildParse传到onObjectParse时已被改掉\n\t\t\t\tif (type == SQLConfig.TYPE_ITEM_CHILD_0 && query != apijson.JSONRequest.QUERY_TABLE && position == 0) {\n\n\t\t\t\t\t//TODO 应在这里判断 @column 中是否有聚合函数，而不是 AbstractSQLConfig.getColumnString\n\n\t\t\t\t\tMap<String, Object> rp;\n\t\t\t\t\tBoolean compat = arrayConfig.getCompat();\n\t\t\t\t\tif (compat != null && compat) {\n\t\t\t\t\t\t// 解决对聚合函数字段通过 query:2 分页查总数返回值错误\n\t\t\t\t\t\t// 这里可能改变了内部的一些数据，下方通过 arrayConfig 还原\n\t\t\t\t\t\tSQLConfig<T, M, L> cfg = op.setSQLConfig(0, 0, 0).getSQLConfig();\n\t\t\t\t\t\tboolean isExplain = cfg.isExplain();\n\t\t\t\t\t\tcfg.setExplain(false);\n\n\t\t\t\t\t\tSubquery subqy = new Subquery();\n\t\t\t\t\t\tsubqy.setFrom(cfg.getTable());\n\t\t\t\t\t\tsubqy.setConfig(cfg);\n\n\t\t\t\t\t\tSQLConfig<T, M, L> countSQLCfg = createSQLConfig();\n\t\t\t\t\t\tcountSQLCfg.setColumn(Arrays.asList(\"count(*):count\"));\n\t\t\t\t\t\tcountSQLCfg.setFrom(subqy);\n\n\t\t\t\t\t\trp = executeSQL(countSQLCfg, false);\n\n\t\t\t\t\t\tcfg.setExplain(isExplain);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\t// 对聚合函数字段通过 query:2 分页查总数返回值错误\n\t\t\t\t\t\tRequestMethod method = op.getMethod();\n\t\t\t\t\t\trp = op.setMethod(RequestMethod.HEAD).setSQLConfig().executeSQL().getSQLResponse();\n\t\t\t\t\t\top.setMethod(method);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (rp != null) {\n\t\t\t\t\t\tint index = parentPath.lastIndexOf(\"]/\");\n\t\t\t\t\t\tif (index >= 0) {\n\t\t\t\t\t\t\tint total = getIntValue(rp, JSONResponse.KEY_COUNT);\n\n\t\t\t\t\t\t\tString pathPrefix = parentPath.substring(0, index) + \"]/\";\n\t\t\t\t\t\t\tputQueryResult(pathPrefix + JSONResponse.KEY_TOTAL, total);\n\n\t\t\t\t\t\t\t//详细的分页信息，主要为 PC 端提供\n\t\t\t\t\t\t\tint count = arrayConfig.getCount();\n\t\t\t\t\t\t\tint page = arrayConfig.getPage();\n\t\t\t\t\t\t\tint max = (int) ((total - 1)/count);\n\t\t\t\t\t\t\tif (max < 0) {\n\t\t\t\t\t\t\t\tmax = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tint min = getMinQueryPage();\n\n\t\t\t\t\t\t\tpage += min;\n\t\t\t\t\t\t\tmax += min;\n\n\t\t\t\t\t\t\tM pagination = JSON.createJSONObject();\n\t\t\t\t\t\t\tObject explain = rp.get(JSONResponse.KEY_EXPLAIN);\n\t\t\t\t\t\t\tif (explain instanceof Map<?, ?>) {\n\t\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_EXPLAIN, explain);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_TOTAL, total);\n\t\t\t\t\t\t\tpagination.put(apijson.JSONRequest.KEY_COUNT, count);\n\t\t\t\t\t\t\tpagination.put(apijson.JSONRequest.KEY_PAGE, page);\n\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_MAX, max);\n\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_MORE, page < max);\n\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_FIRST, page == min);\n\t\t\t\t\t\t\tpagination.put(JSONResponse.KEY_LAST, page == max);\n\n\t\t\t\t\t\t\tputQueryResult(pathPrefix + JSONResponse.KEY_INFO, pagination);\n\n\t\t\t\t\t\t\tif (total <= count*(page - min)) {\n\t\t\t\t\t\t\t\tquery = apijson.JSONRequest.QUERY_TOTAL;//数量不够了，不再往后查询\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\top.setMethod(requestMethod);\n\t\t\t\t}\n\n\t\t\t\t//Table\n\t\t\t\tif (query == apijson.JSONRequest.QUERY_TOTAL) {\n\t\t\t\t\tresponse = null;//不再往后查询\n\t\t\t\t} else {\n\t\t\t\t\tresponse = op\n\t\t\t\t\t\t\t.setSQLConfig(arrayConfig.getCount(), arrayConfig.getPage(), position)\n\t\t\t\t\t\t\t.executeSQL()\n\t\t\t\t\t\t\t.response();\n\t\t\t\t\t//\t\t\t\t\titemConfig = op.getConfig();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isArrayMainTable) {\n\t\t\t\tif (position == 0) {  // 提取并缓存数组主表的列表数据\n\t\t\t\t\tarrayObjectParserCacheMap.put(parentPath.substring(0, parentPath.lastIndexOf(\"[]\") + 2), op);\n\t\t\t\t}\n\t\t\t}\n\t\t\t//\t\t\telse {\n\t\t\t//\t\t\t\top.recycle();\n\t\t\t//\t\t\t}\n\t\t\top = null;\n\t\t}\n\n\t\treturn response;\n\t}\n\n\t/**获取对象数组，该对象数组处于parentObject内\n\t * @param request parentObject的value\n\t * @param parentPath parentObject的路径\n\t * @param name parentObject的key\n\t * @param isSubquery 是否为子查询\n\t * @param cache SQL 结果缓存\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic L onArrayParse(M request, String parentPath, String name, boolean isSubquery, L cache) throws Exception {\n\t\tif (Log.DEBUG) {\n\t\t\tLog.i(TAG, \"\\n\\n\\n onArrayParse parentPath = \" + parentPath\n\t\t\t\t\t+ \"; name = \" + name + \"; request = \" + JSON.toJSONString(request));\n\t\t}\n\n\t\t//不能允许GETS，否则会被通过\"[]\":{\"@role\":\"ADMIN\"},\"Table\":{},\"tag\":\"Table\"绕过权限并能批量查询\n\t\tRequestMethod _method = request.get(KEY_METHOD) == null ? requestMethod : RequestMethod.valueOf(getString(request, KEY_METHOD));\n\t\tif (isSubquery == false && RequestMethod.isGetMethod(_method, true) == false) {\n\t\t\tthrow new UnsupportedOperationException(\"key[]:{} 只支持 GET, GETS 方法！其它方法不允许传 \" + name + \":{} 等这种 key[]:{} 格式！\");\n\t\t}\n\t\tif (request == null || request.isEmpty()) { // jsonKey-jsonValue 条件\n\t\t\treturn null;\n\t\t}\n\t\tString path = getAbsPath(parentPath, name);\n\n\n\t\t//不能改变，因为后面可能继续用到，导致1以上都改变 []:{0:{Comment[]:{0:{Comment:{}},1:{...},...}},1:{...},...}\n\t\tfinal String query = getString(request, apijson.JSONRequest.KEY_QUERY);\n\t\tfinal Boolean compat = getBoolean(request, apijson.JSONRequest.KEY_COMPAT);\n\t\tfinal Integer count = getInteger(request, apijson.JSONRequest.KEY_COUNT); //TODO 如果不想用默认数量可以改成 getIntValue(apijson.JSONRequest.KEY_COUNT);\n\t\tfinal Integer page = getInteger(request, apijson.JSONRequest.KEY_PAGE);\n\t\tfinal Object join = request.get(apijson.JSONRequest.KEY_JOIN);\n\n\t\tint query2;\n\t\tif (query == null) {\n\t\t\tquery2 = apijson.JSONRequest.QUERY_TABLE;\n\t\t}\n\t\telse {\n\t\t\tswitch (query) {\n\t\t\tcase \"0\":\n\t\t\tcase apijson.JSONRequest.QUERY_TABLE_STRING:\n\t\t\t\tquery2 = apijson.JSONRequest.QUERY_TABLE;\n\t\t\t\tbreak;\n\t\t\tcase \"1\":\n\t\t\tcase apijson.JSONRequest.QUERY_TOTAL_STRING:\n\t\t\t\tquery2 = apijson.JSONRequest.QUERY_TOTAL;\n\t\t\t\tbreak;\n\t\t\tcase \"2\":\n\t\t\tcase apijson.JSONRequest.QUERY_ALL_STRING:\n\t\t\t\tquery2 = apijson.JSONRequest.QUERY_ALL;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalArgumentException(path + \"/\" + apijson.JSONRequest.KEY_QUERY + \":value 中 value 的值不合法！必须在 [0, 1, 2] 或 [TABLE, TOTAL, ALL] 内 !\");\n\t\t\t}\n\t\t}\n\n\t\tint minPage = getMinQueryPage(); // 兼容各种传 0 或 null/undefined 自动转 0 导致的问题\n\t\tint page2 = page == null || page == 0 ? 0 : page - minPage;\n\n\t\tint maxPage = getMaxQueryPage();\n\t\tif (page2 < 0 || page2 > maxPage) {\n\t\t\tthrow new IllegalArgumentException(path + \"/\" + apijson.JSONRequest.KEY_PAGE + \":value 中 value 的值不合法！必须在 \" + minPage + \"-\" + maxPage + \" 内 !\");\n\t\t}\n\n\t\t//不用total限制数量了，只用中断机制，total只在query = 1,2的时候才获取\n\t\tint count2 = isSubquery || count != null ? (count == null ? 0 : count) : getDefaultQueryCount();\n\t\tint max = isSubquery ? count2 : getMaxQueryCount();\n\n\t\tif (count2 < 0 || count2 > max) {\n\t\t\tthrow new IllegalArgumentException(path + \"/\" + apijson.JSONRequest.KEY_COUNT + \":value 中 value 的值不合法！必须在 0-\" + max + \" 内 !\");\n\t\t}\n\n\t\trequest.remove(apijson.JSONRequest.KEY_QUERY);\n\t\trequest.remove(apijson.JSONRequest.KEY_COMPAT);\n\t\trequest.remove(apijson.JSONRequest.KEY_COUNT);\n\t\trequest.remove(apijson.JSONRequest.KEY_PAGE);\n\t\trequest.remove(apijson.JSONRequest.KEY_JOIN);\n\t\tLog.d(TAG, \"onArrayParse  query = \" + query + \"; count = \" + count + \"; page = \" + page + \"; join = \" + join);\n\n\t\tif (request.isEmpty()) { // 如果条件成立，说明所有的 parentPath/name:request 中request都无效！！！ 后续都不执行，没必要还原数组关键词浪费性能\n\t\t\tLog.e(TAG, \"onArrayParse  request.isEmpty() >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\tL response = null;\n\t\ttry {\n\t\t\tint size = count2 == 0 ? max : count2; //count为每页数量，size为第page页实际数量，max(size) = count\n\t\t\tLog.d(TAG, \"onArrayParse  size = \" + size + \"; page = \" + page2);\n\n\n\t\t\t//key[]:{Table:{}}中key equals Table时 提取Table\n\t\t\tint index = isSubquery || name == null ? -1 : name.lastIndexOf(\"[]\");\n\t\t\tString childPath = index <= 0 ? null : Pair.parseEntry(name.substring(0, index), true).getKey(); // Table-key1-key2...\n\n\t\t\tString arrTableKey = null;\n\t\t\t//判断第一个key，即Table是否存在，如果存在就提取\n\t\t\tString[] childKeys = StringUtil.split(childPath, \"-\", false);\n\t\t\tif (childKeys == null || childKeys.length <= 0 || request.containsKey(childKeys[0]) == false) {\n\t\t\t\tchildKeys = null;\n\t\t\t}\n\t\t\telse if (childKeys.length == 1 && isTableKey(childKeys[0])) {  // 可能无需提取，直接返回 rawList 即可\n\t\t\t\tarrTableKey = childKeys[0];\n\t\t\t}\n\n\n\t\t\t//Table<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t\t\tList<Join<T, M, L>> joinList = onJoinParse(join, request);\n\t\t\tSQLConfig<T, M, L> config = createSQLConfig()\n\t\t\t\t\t.setMethod(requestMethod)\n\t\t\t\t\t.setCount(size)\n\t\t\t\t\t.setPage(page2)\n\t\t\t\t\t.setQuery(query2)\n\t\t\t\t\t.setCompat(compat)\n\t\t\t\t\t.setTable(arrTableKey)\n\t\t\t\t\t.setJoinList(joinList);\n\n\t\t\tMap<String, Object> parent;\n\n\t\t\tboolean isExtract = true;\n\n\t\t\tresponse = JSON.createJSONArray();\n\t\t\t//生成size个\n\t\t\tfor (int i = 0; i < (isSubquery ? 1 : size); i++) {\n\t\t\t\tparent = onObjectParse(request, isSubquery ? parentPath : path, isSubquery ? name : \"\" + i, config.setType(SQLConfig.TYPE_ITEM).setPosition(i), isSubquery, null);\n\t\t\t\tif (parent == null || parent.isEmpty()) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tlong startTime = System.currentTimeMillis();\n\n\t\t\t\t/* 这里优化了 Table[]: { Table:{} } 这种情况下的性能\n\t\t\t\t * 如果把 List<Map<String, Object>> 改成 L 来减少以下 addAll 一次复制，则会导致 AbstractSQLExecutor<T, M, L> 等其它很多地方 get 要改为 getJSONObject，\n\t\t\t\t * 修改类型会导致不兼容旧版依赖 ORM 的项目，而且整体上性能只有特殊情况下性能提升，其它非特殊情况下因为多出很多 instanceof Map<?, ?> 的判断而降低了性能。\n\t\t\t\t */\n\t\t\t\tMap<String, Object> fo = i != 0 || arrTableKey == null ? null : JSON.get(parent, arrTableKey);\n\t\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\t\tList<Map<String, Object>> list = fo == null ? null : (List<Map<String, Object>>) fo.remove(AbstractSQLExecutor.KEY_RAW_LIST);\n\n\t\t\t\tif (list != null && list.isEmpty() == false && (joinList == null || joinList.isEmpty())) {\n\t\t\t\t\tisExtract = false;\n\n\t\t\t\t\tlist.set(0, fo);  // 不知道为啥第 0 项也加了 @RAW@LIST\n\t\t\t\t\tresponse.addAll(list);  // List<Map<String, Object>> cannot match List<Object>   response = JSON.createJSONArray(list);\n\n\t\t\t\t\tlong endTime = System.currentTimeMillis();  // 0ms\n\t\t\t\t\tLog.d(TAG, \"\\n onArrayParse <<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n for (int i = 0; i < (isSubquery ? 1 : size); i++) \"\n\t\t\t\t\t\t\t+ \" startTime = \" + startTime + \"; endTime = \" + endTime + \"; duration = \" + (endTime - startTime) + \"\\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\");\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\t//key[]:{Table:{}}中key equals Table时 提取Table\n\t\t\t\tresponse.add(getValue(parent, childKeys)); //null有意义\n\t\t\t}\n\n\t\t\t//Table>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t\t\t/*\n\t\t\t * 支持引用取值后的数组\n\t\t\t{\n\t\t\t    \"User-id[]\": {\n\t\t\t        \"User\": {\n\t\t\t            \"contactIdList<>\": 82002\n\t\t\t        }\n\t\t\t    },\n\t\t\t    \"Moment-userId[]\": {\n\t\t\t        \"Moment\": {\n\t\t\t            \"userId{}@\": \"User-id[]\"\n\t\t\t        }\n\t\t\t    }\n\t\t\t}\n\t\t\t */\n\t\t\tif (isExtract) {\n\t\t\t\tlong startTime = System.currentTimeMillis();\n\n\t\t\t\tObject fo = childKeys == null || response.isEmpty() ? null : response.get(0);\n\t\t\t\tif (fo instanceof Boolean || fo instanceof Number || fo instanceof String) { //[{}] 和 [[]] 都没意义\n\t\t\t\t\tputQueryResult(path, response);\n\t\t\t\t}\n\n\t\t\t\tlong endTime = System.currentTimeMillis();\n\t\t\t\tLog.d(TAG, \"\\n onArrayParse <<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n isExtract >> putQueryResult \"\n\t\t\t\t\t\t+ \" startTime = \" + startTime + \"; endTime = \" + endTime + \"; duration = \" + (endTime - startTime) + \"\\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\");\n\t\t\t}\n\n\t\t} finally {\n\t\t\t//后面还可能用到，要还原\n\t\t\trequest.put(apijson.JSONRequest.KEY_QUERY, query);\n\t\t\trequest.put(apijson.JSONRequest.KEY_COMPAT, compat);\n\t\t\trequest.put(apijson.JSONRequest.KEY_COUNT, count);\n\t\t\trequest.put(apijson.JSONRequest.KEY_PAGE, page);\n\t\t\trequest.put(apijson.JSONRequest.KEY_JOIN, join);\n\t\t}\n\n\t\tif (Log.DEBUG) {\n\t\t\tLog.i(TAG, \"onArrayParse  return response = \\n\" + JSON.toJSONString(response) + \"\\n>>>>>>>>>>>>>>>\\n\\n\\n\");\n\t\t}\n\t\treturn response;\n\t}\n\n\n\n\tprivate static final List<String> JOIN_COPY_KEY_LIST;\n\tstatic {  // TODO 不全\n\t\tJOIN_COPY_KEY_LIST = new ArrayList<String>();\n\t\tJOIN_COPY_KEY_LIST.add(KEY_ROLE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_DATABASE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_NAMESPACE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_CATALOG);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_SCHEMA);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_DATASOURCE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_COLUMN);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_NULL);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_CAST);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_COMBINE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_GROUP);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_HAVING);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_HAVING_AND);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_SAMPLE);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_LATEST);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_PARTITION);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_FILL);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_ORDER);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_KEY);\n\t\tJOIN_COPY_KEY_LIST.add(KEY_RAW);\n\t}\n\n\t/**JOIN 多表同时筛选\n\t * @param join \"&/User,</Moment/id@\",@/Comment/toId@\" 或 \"&/User\":{}, \"</Moment/id@\":{\"@column\":\"id\"}, \"@/Comment/toId@\": {\"@group\":\"toId\", \"@having\":\"toId>0\"}\n\t * @param request\n\t * @return\n\t * @throws Exception\n\t */\n\tprivate List<Join<T, M, L>> onJoinParse(Object join, M request) throws Exception {\n\t\tMap<String, Object> joinMap = null;\n\n\t\tif (join instanceof Map<?, ?>) {\n\t\t\tjoinMap = (M) join;\n\t\t}\n\t\telse if (join instanceof String) {\n\t\t\tString[] sArr = request == null || request.isEmpty() ? null : StringUtil.split((String) join);\n\t\t\tif (sArr != null && sArr.length > 0) {\n\t\t\t\tjoinMap = new LinkedHashMap<String, Object>(); //注意：这里必须要保证join连接顺序，保证后边遍历是按照join参数的顺序生成的SQL\n\t\t\t\tfor (int i = 0; i < sArr.length; i++) {\n\t\t\t\t\tjoinMap.put(sArr[i], new LinkedHashMap<String, Object>());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\telse if (join != null){\n\t\t\tthrow new UnsupportedDataTypeException(TAG + \".onJoinParse  join 只能是 String 或 Map<String, Object> 类型！\");\n\t\t}\n\n\t\tList<Entry<String, Object>> slashKeys = new ArrayList<>();\n\t\tList<Entry<String, Object>> nonSlashKeys = new ArrayList<>();\n\t\tSet<Entry<String, Object>> entries = joinMap == null ? null : joinMap.entrySet();\n\n\t\tif (entries == null || entries.isEmpty()) {\n\t\t\tLog.e(TAG, \"onJoinParse  set == null || set.isEmpty() >> return null;\");\n\t\t\treturn null;\n\t\t}\n\t\tfor (Entry<String, Object> e : entries) {\n\t\t\tString path = e.getKey();\n\t\t\tif (path != null && path.indexOf(\"/\") > 0) {\n\t\t\t\tslashKeys.add(e);  // 以 / 开头的 key，例如 </Table/key@\n\t\t\t} else {\n\t\t\t\tnonSlashKeys.add(e);  // 普通 key，例如 Table: {}\n\t\t\t}\n\t\t}\n\n\t\tMap<String, Object> whereJoinMap = new LinkedHashMap<>();\n\n\t\tfor (Entry<String, Object> e : nonSlashKeys) {\n\t\t\tString tableKey = e.getKey(); // 如 \"Location_info\"\n\t\t\tObject tableObj = e.getValue(); // value 是 Map\n\n\t\t\tif (request.containsKey(tableKey)) {\n\t\t\t\twhereJoinMap.put(tableKey, tableObj);\n\t\t\t} else {\n\t\t\t\tLog.w(TAG, \"跳过 join 中 key = \" + tableKey + \"，因为它不在 request 中\");\n\t\t\t}\n\t\t}\n\n\n\t\tSet<Entry<String, Object>> set = joinMap == null ? null : new LinkedHashSet<>(slashKeys);\n\n\t\tif (set == null || set.isEmpty()) {\n\t\t\tLog.e(TAG, \"onJoinParse  set == null || set.isEmpty() >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\tList<Join<T, M, L>> joinList = new ArrayList<>();\n\n\t\tfor (Entry<String, Object> e : set) {  // { &/User:{}, </Moment/id@\":{}, @/Comment/toId@:{} }\n\t\t\t// 分割 /Table/key\n\t\t\tString path = e == null ? null : e.getKey();\n\t\t\tObject outer = path == null ? null : e.getValue();\n\n\t\t\tif (outer instanceof Map<?, ?> == false) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中value不合法！\"\n\t\t\t\t\t\t+ \"必须为 &/Table0/key0,</Table1/key1,... 或 { '&/Table0/key0':{}, '</Table1/key1':{},... } 这种形式！\");\n\t\t\t}\n\n\t\t\tint index = path == null ? -1 : path.indexOf(\"/\");\n\t\t\tif (index < 0) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中 value 值 \" + path + \" 不合法！\"\n\t\t\t\t\t\t+ \"必须为 &/Table0,</Table1/key1,@/Table1:alias2/key2,... 或 { '&/Table0':{}, '</Table1/key1':{},... } 这种形式！\");\n\t\t\t}\n\t\t\tString joinType = path.substring(0, index); //& | ! < > ( ) <> () *\n\t\t\t//\t\t\tif (StringUtil.isEmpty(joinType, true)) {\n\t\t\t//\t\t\t\tjoinType = \"|\"; // FULL JOIN\n\t\t\t//\t\t\t}\n\t\t\tpath = path.substring(index + 1);\n\n\t\t\tindex = path.lastIndexOf(\"/\");\n\t\t\tString tableKey = index < 0 ? path : path.substring(0, index); // User:owner\n\t\t\tint index2 = tableKey.lastIndexOf(\"/\");\n\t\t\tString arrKey = index2 < 0 ? null : tableKey.substring(0, index2);\n\t\t\tif (arrKey != null && isArrayKey(arrKey) == false) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 对应的 \" + arrKey + \" 不是合法的数组 key[] ！\" +\n\t\t\t\t\t\t\"@ APP JOIN 最多允许跨 1 层，只能是子数组，且数组对象中不能有 join: value 键值对！\");\n\t\t\t}\n\n\t\t\ttableKey = index2 < 0 ? tableKey : tableKey.substring(index2+1);\n\n\t\t\tapijson.orm.Entry<String, String> entry = Pair.parseEntry(tableKey, true);\n\t\t\tString table = entry.getKey(); // User\n\t\t\tif (StringUtil.isName(table) == false) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中 value 的 Table 值 \" + table + \" 不合法！\"\n\t\t\t\t\t\t+ \"必须为 &/Table0,</Table1/key1,@/Table1:alias2/key2,... 或 { '&/Table0':{}, '</Table1/key1':{},... } 这种格式！\"\n\t\t\t\t\t\t+ \"且 Table 必须满足大写字母开头的表对象英文单词 key 格式！\");\n\t\t\t}\n\n\t\t\tString alias = entry.getValue(); // owner\n\t\t\tif (StringUtil.isNotEmpty(alias, true) && StringUtil.isName(alias) == false) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中 value 的 alias 值 \" + alias + \" 不合法！\"\n\t\t\t\t\t\t+ \"必须为 &/Table0,</Table1/key1,@/Table1:alias2/key2,... 或 { '&/Table0':{}, '</Table1/key1':{},... } 这种格式！\"\n\t\t\t\t\t\t+ \"且 Table:alias 的 alias 必须满足英文单词变量名格式！\");\n\t\t\t}\n\n\t\t\t// 取出Table对应的JSONObject，及内部引用赋值 key:value\n\t\t\tM tableObj;\n\t\t\tM parentPathObj;\t// 保留\n\t\t\ttry {\n\t\t\t\tparentPathObj = arrKey == null ? request : JSON.get(request, arrKey);\t// 保留\n\t\t\t\ttableObj = parentPathObj == null ? null : JSON.get(parentPathObj, tableKey);\n\t\t\t\tif (tableObj == null) {\n\t\t\t\t\tthrow new NullPointerException(\"tableObj == null\");\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (Exception e2) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 对应的 \" + tableKey + \":value 中 value 类型不合法！\" +\n          \t\t\t\"必须是 {} 这种 Map<String, Object> 格式！\" + e2.getMessage());\n\t\t\t}\n\n\t\t\tif (arrKey != null) {\n\t\t\t\tif (parentPathObj.get(apijson.JSONRequest.KEY_JOIN) != null) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 对应的 \" + arrKey + \":{ join: value } 中 value 不合法！\" +\n\t\t\t\t\t\t\t\"@ APP JOIN 最多允许跨 1 层，只能是子数组，且数组对象中不能有 join: value 键值对！\");\n\t\t\t\t}\n\n\t\t\t\tInteger subPage = getInteger(parentPathObj, apijson.JSONRequest.KEY_PAGE);\n\t\t\t\tif (subPage != null && subPage != 0) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 对应的 \" + arrKey + \":{ page: value } 中 value 不合法！\" +\n\t\t\t\t\t\t\t\"@ APP JOIN 最多允许跨 1 层，只能是子数组，且数组对象中 page 值只能为 null 或 0 ！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tboolean isAppJoin = \"@\".equals(joinType);\n\n\t\t\tM refObj = JSON.createJSONObject();\n\n\t\t\tString key = index < 0 ? null : path.substring(index + 1); // id@\n\t\t\tif (key != null) {  // 指定某个 key 为 JOIN ON 条件\n\t\t\t\tif (key.indexOf(\"@\") != key.length() - 1) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":\" + e.getKey() + \" 中 \" + key + \" 不合法！\"\n\t\t\t\t\t\t\t+ \"必须为 &/Table0,</Table1/key1,@/Table1:alias2/key2,... 或 { '&/Table0':{}, '</Table1/key1':{},... } 这种格式！\"\n\t\t\t\t\t\t\t+ \"且 Table:alias 的 alias 必须满足英文单词变量名格式！\");\n\t\t\t\t}\n\n\t\t\t\tif (tableObj.get(key) instanceof String == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":\" + e.getKey() + \"' 对应的 \"\n            \t\t\t+ tableKey + \":{ \" + key + \": value } 中 value 类型不合法！必须为同层级引用赋值路径 String！\");\n\t\t\t\t}\n\n\t\t\t\tif (isAppJoin && StringUtil.isName(key.substring(0, key.length() - 1)) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 中 \" + key + \" 不合法 ！\" +\n\t\t\t\t\t\t\t\"@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！\");\n\t\t\t\t}\n\n\t\t\t\trefObj.put(key, getString(tableObj, key));\n\t\t\t}\n\n\n\t\t\tSet<Entry<String, Object>> tableSet = tableObj.entrySet();\n\t\t\t// 取出所有 join 条件\n\t\t\tM requestObj = JSON.createJSONObject(); // (Map<String, Object>) obj.clone();\n\n\t\t\tboolean matchSingle = false;\n\t\t\tfor (Entry<String, Object> tableEntry : tableSet) {\n\t\t\t\tString k = tableEntry.getKey();\n\t\t\t\tObject v = k == null ? null : tableEntry.getValue();\n\t\t\t\tif (v == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tmatchSingle = matchSingle == false && k.equals(key);\n\t\t\t\tif (matchSingle) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (k.length() > 1 && k.indexOf(\"@\") == k.length() - 1 && v instanceof String) {\n\t\t\t\t\tString sv = (String) v;\n\t\t\t\t\tint ind = sv.endsWith(\"@\") ? -1 : sv.indexOf(\"/\");\n\t\t\t\t\tif (ind == 0 && key == null) {  // 指定了某个就只允许一个 ON 条件\n\t\t\t\t\t\tString p = sv.substring(1);\n\t\t\t\t\t\tint ind2 = p.indexOf(\"/\");\n\t\t\t\t\t\tString tk = ind2 < 0 ? null : p.substring(0, ind2);\n\n\t\t\t\t\t\tapijson.orm.Entry<String, String> te = tk == null || p.substring(ind2 + 1).indexOf(\"/\") >= 0 ? null : Pair.parseEntry(tk, true);\n\n\t\t\t\t\t\tif (te != null && isTableKey(te.getKey()) && request.get(tk) instanceof Map<?, ?>) {\n\t\t\t\t\t\t\tif (isAppJoin) {\n\t\t\t\t\t\t\t\tif (refObj.size() >= 1) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":\" + e.getKey() + \" 中 \" + k + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"@ APP JOIN 必须有且只有一个引用赋值键值对！\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tif (StringUtil.isName(k.substring(0, k.length() - 1)) == false) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":'\" + e.getKey() + \"' 中 \" + k + \" 不合法 ！\" +\n\t\t\t\t\t\t\t\t\t\t\t\"@ APP JOIN 只允许 key@:/Table/refKey 这种 = 等价连接！\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\trefObj.put(k, v);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tObject rv = getValueByPath(sv);\n\t\t\t\t\tif (rv != null && rv.equals(sv) == false) {\n\t\t\t\t\t\trequestObj.put(k.substring(0, k.length() - 1), rv);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tthrow new UnsupportedOperationException(table + \"/\" + k + \" 不合法！\" + apijson.JSONRequest.KEY_JOIN + \" 关联的 Table 中，\"\n\t\t\t\t\t\t\t+ \"join: ?/Table/key 时只能有 1 个 key@:value；join: ?/Table 时所有 key@:value 要么是符合 join 格式，要么能直接解析成具体值！\");  // TODO 支持 join on\n\t\t\t\t}\n\n\t\t\t\tif (k.startsWith(\"@\")) {\n\t\t\t\t\tif (JOIN_COPY_KEY_LIST.contains(k)) {\n\t\t\t\t\t\trequestObj.put(k, v); // 保留\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tif (k.endsWith(\"@\")) {\n\t\t\t\t\t\tthrow new UnsupportedOperationException(table + \"/\" + k + \" 不合法！\" + apijson.JSONRequest.KEY_JOIN + \" 关联的 Table 中，\"\n\t\t\t\t\t\t\t\t+ \"join: ?/Table/key 时只能有 1 个 key@:value；join: ?/Table 时所有 key@:value 要么是符合 join 格式，要么能直接解析成具体值！\");  // TODO 支持 join on\n\t\t\t\t\t}\n\n\t\t\t\t\tif (k.contains(\"()\") == false) { // 不需要远程函数\n\t\t\t\t\t\trequestObj.put(k, v); // 保留\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tSet<Entry<String, Object>> refSet = refObj.entrySet();\n\t\t\tif (refSet.isEmpty() && \"*\".equals(joinType) == false) {\n\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中 value 的 alias 值 \" + alias + \" 不合法！\"\n\t\t\t\t\t\t+ \"必须为 &/Table0,</Table1/key1,@/Table1:alias2/key2,... 或 { '&/Table0':{}, '</Table1/key1':{},... } 这种格式！\"\n\t\t\t\t\t\t+ \"且 Table:alias 的 alias 必须满足英文单词变量名格式！\");\n\t\t\t}\n\n\n\t\t\tJoin<T, M, L> j = new Join<>();\n\t\t\tj.setPath(e.getKey());\n\t\t\tj.setJoinType(joinType);\n\t\t\tj.setTable(table);\n\t\t\tj.setAlias(alias);\n\n\t\t\tM outerObj = (M) JSON.createJSONObject((Map<String, Object>) outer);\n            j.setOn(outerObj);\n\t\t\tj.setRequest(requestObj);\n\n            if (whereJoinMap.containsKey(table)) {\n                Object rawOuter = whereJoinMap.get(table);\n                M outerObj1 = (M) JSON.createJSONObject((Map<String, Object>) rawOuter);\n                j.setOuter(outerObj1);\n            }\n\n\t\t\tif (arrKey != null) {\n\t\t\t\tInteger count = getInteger(parentPathObj, apijson.JSONRequest.KEY_COUNT);\n\t\t\t\tj.setCount(count == null ? getDefaultQueryCount() : count);\n\t\t\t}\n\n\t\t\tList<Join.On> onList = new ArrayList<>();\n\t\t\tfor (Entry<String, Object> refEntry : refSet) {\n\t\t\t\tString originKey = refEntry.getKey();\n\n\t\t\t\tString targetPath = (String) refEntry.getValue();\n\t\t\t\tif (StringUtil.isEmpty(targetPath, true)) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":value 中 value 值 \" + targetPath + \" 不合法！必须为引用赋值的路径 '/targetTable/targetKey' ！\");\n\t\t\t\t}\n\n\t\t\t\t// 取出引用赋值路径 targetPath 对应的 Table 和 key\n\t\t\t\tindex = targetPath.lastIndexOf(\"/\");\n\t\t\t\tString targetKey = index < 0 ? null : targetPath.substring(index + 1);\n\t\t\t\tif (StringUtil.isName(targetKey) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable/targetKey' 中 targetKey 值 \" + targetKey + \" 不合法！必须满足英文单词变量名格式！\");\n\t\t\t\t}\n\n\t\t\t\ttargetPath = targetPath.substring(0, index);\n\t\t\t\tindex = targetPath.lastIndexOf(\"/\");\n\t\t\t\tString targetTableKey = index < 0 ? targetPath : targetPath.substring(index + 1);\n\n\t\t\t\t// 主表允许别名\n\t\t\t\tapijson.orm.Entry<String, String> targetEntry = Pair.parseEntry(targetTableKey, true);\n\t\t\t\tString targetTable = targetEntry.getKey(); //User\n\t\t\t\tif (StringUtil.isName(targetTable) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable/targetKey' 中 targetTable 值 \" + targetTable + \" 不合法！必须满足大写字母开头的表对象英文单词 key 格式！\");\n\t\t\t\t}\n\n\t\t\t\tString targetAlias = targetEntry.getValue(); //owner\n\t\t\t\tif (StringUtil.isNotEmpty(targetAlias, true) && StringUtil.isName(targetAlias) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable:targetAlias/targetKey' 中 targetAlias 值 \" + targetAlias + \" 不合法！必须满足英文单词变量名格式！\");\n\t\t\t\t}\n\n\t\t\t\t//targetTable = targetTableKey;  // 主表允许别名\n\t\t\t\tif (StringUtil.isName(targetTable) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable/targetKey' 中 targetTable 值 \" + targetTable + \" 不合法！必须满足大写字母开头的表对象英文单词 key 格式！\");\n\t\t\t\t}\n\n\t\t\t\t//对引用的JSONObject添加条件\n\t\t\t\tMap<String, Object> targetObj;\n\t\t\t\ttry {\n\t\t\t\t\ttargetObj = JSON.get(request, targetTableKey);\n\t\t\t\t}\n\t\t\t\tcatch (Exception e2) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable/targetKey' 中路径对应的 '\" + targetTableKey + \"':value 中 value 类型不合法！必须是 {} 这种 Map<String, Object> 格式！\" + e2.getMessage());\n\t\t\t\t}\n\n\t\t\t\tif (targetObj == null) {\n\t\t\t\t\tthrow new IllegalArgumentException(e.getKey() + \":'/targetTable/targetKey' 中路径对应的对象 '\" + targetTableKey + \"':{} 不存在或值为 null ！必须是 {} 这种 Map<String, Object> 格式！\");\n\t\t\t\t}\n\n\t\t\t\tJoin.On on = new Join.On();\n\t\t\t\ton.setKeyAndType(j.getJoinType(), j.getTable(), originKey);\n\t\t\t\tif (StringUtil.isName(on.getKey()) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(apijson.JSONRequest.KEY_JOIN + \":value 中 value 的 key@ 中 key 值 \" + on.getKey() + \" 不合法！必须满足英文单词变量名格式！\");\n\t\t\t\t}\n\n\t\t\t\ton.setOriginKey(originKey);\n\t\t\t\ton.setOriginValue((String) refEntry.getValue());\n\t\t\t\ton.setTargetTableKey(targetTableKey);\n\t\t\t\ton.setTargetTable(targetTable);\n\t\t\t\ton.setTargetAlias(targetAlias);\n\t\t\t\ton.setTargetKey(targetKey);\n\n\t\t\t\tonList.add(on);\n\t\t\t}\n\n\t\t\tj.setOnList(onList);\n\n\t\t\tjoinList.add(j);\n\t\t\t//\t\t\tonList.add(table + \".\" + key + \" = \" + targetTable + \".\" + targetKey); // ON User.id = Moment.userId\n\n\t\t\t// 保证和 SQLExcecutor 缓存的 Config 里 where 顺序一致，生成的 SQL 也就一致 <<<<<<<<<\n\t\t\t// AbstractSQLConfig.newSQLConfig<T, M, L> 中强制把 id, id{}, userId, userId{} 放到了最前面\t\ttableObj.put(key, tableObj.remove(key));\n\n\t\t\tif (refObj.size() != tableObj.size()) {  // 把 key 强制放最前，AbstractSQLExcecutor 中 config.putWhere 也是放尽可能最前\n\t\t\t\trefObj.putAll(tableObj);\n\t\t\t\tparentPathObj.put(tableKey, refObj);\n\n//\t\t\t\ttableObj.clear();\n//\t\t\t\ttableObj.putAll(refObj);\n\t\t\t}\n\t\t\t// 保证和 SQLExcecutor 缓存的 Config 里 where 顺序一致，生成的 SQL 也就一致 >>>>>>>>>\n\t\t}\n\n\t\t//拼接多个 SQLConfig<T, M, L> 的SQL语句，然后执行，再把结果分别缓存(Moment, User等)到 SQLExecutor<T, M, L> 的 cacheMap\n\t\t//\t\tAbstractSQLConfig<T, M, L> config0 = null;\n\t\t//\t\tString sql = \"SELECT \" + config0.getColumnString() + \" FROM \" + config0.getTable() + \" INNER JOIN \" + targetTable + \" ON \"\n\t\t//\t\t\t\t+ onList.get(0) + config0.getGroupString() + config0.getHavingString() + config0.getOrderString();\n\n\t\treturn joinList;\n\t}\n\n\t/**根据路径取值\n\t * @param parent\n\t * @param pathKeys\n\t * @return\n\t */\n\tpublic static <V extends Object> V getValue(Object parent, String[] pathKeys) {\n\t\tint len = parent == null || pathKeys == null ? 0 : pathKeys.length;\n\t\tif (len <= 0) {\n\t\t\tLog.w(TAG, \"getChild  parent == null || pathKeys == null || pathKeys.length <= 0 >> return parent;\");\n\t\t\treturn (V) parent;\n\t\t}\n\n\t\t// 逐层到达child的直接容器JSONObject parent\n\t\tObject v = parent;\n\t\tfor (int i = 0; i < len; i++) { // 一步一步到达指定位置\n\t\t\tif (v == null) { // 不存在或路径错误(中间的key对应value不是JSONObject)\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tString k = getDecodedKey(pathKeys[i]);\n\t\t\ttry {\n\t\t\t\tv = getFromObjOrArr(v, k);\n\t\t\t} catch (Throwable e) {\n\t\t\t\tif (IS_PRINT_BIG_LOG) {\n\t\t\t\t\tLog.e(TAG, \"getFromObjOrArr failed\", e);\n\t\t\t\t}\n\t\t\t\tv = null;\n\t\t\t}\n\t\t}\n\n\t\treturn (V) v;\n\t}\n\n\n\t/**获取被依赖引用的key的路径, 实时替换[] -> []/i\n\t * @param parentPath\n\t * @param valuePath\n\t * @return\n\t */\n\tpublic static String getValuePath(String parentPath, String valuePath) {\n\t\tif (valuePath.startsWith(\"/\")) {\n\t\t\tvaluePath = getAbsPath(parentPath, valuePath);\n\t\t} else {//处理[] -> []/i\n\t\t\tvaluePath = replaceArrayChildPath(parentPath, valuePath);\n\t\t}\n\t\treturn valuePath;\n\t}\n\n\t/**获取绝对路径\n\t * @param path\n\t * @param name\n\t * @return\n\t */\n\tpublic static String getAbsPath(String path, String name) {\n\t\tLog.i(TAG, \"getPath  path = \" + path + \"; name = \" + name + \" <<<<<<<<<<<<<\");\n\t\tpath = StringUtil.get(path);\n\t\tname = StringUtil.get(name);\n\t\tif (StringUtil.isNotEmpty(path, false)) {\n\t\t\tif (StringUtil.isNotEmpty(name, false)) {\n\t\t\t\tpath += ((name.startsWith(\"/\") ? \"\" : \"/\") + name);\n\t\t\t}\n\t\t} else {\n\t\t\tpath = name;\n\t\t}\n\t\tif (path.startsWith(\"/\")) {\n\t\t\tpath = path.substring(1);\n\t\t}\n\t\tLog.i(TAG, \"getPath  return \" + path + \" >>>>>>>>>>>>>>>>\");\n\t\treturn path;\n\t}\n\n\t/**替换[] -> []/i\n\t * 不能写在getAbsPath里，因为name不一定是依赖路径\n\t * @param parentPath\n\t * @param valuePath\n\t * @return\n\t */\n\tpublic static String replaceArrayChildPath(String parentPath, String valuePath) {\n\t\tString[] ps = StringUtil.split(parentPath, \"]/\");//\"[]/\");\n\t\tif (ps != null && ps.length > 1) {\n\t\t\tString[] vs = StringUtil.split(valuePath, \"]/\");\n\n\t\t\tif (vs != null && vs.length > 0) {\n\t\t\t\tString pos;\n\t\t\t\tfor (int i = 0; i < ps.length - 1; i++) {\n\t\t\t\t\tif (ps[i] == null || ps[i].equals(vs[i]) == false) {//允许\"\"？\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tpos = ps[i+1].contains(\"/\") == false ? ps[i+1]\n\t\t\t\t\t\t\t: ps[i+1].substring(0, ps[i+1].indexOf(\"/\"));\n\t\t\t\t\tif (\n\t\t\t\t\t\t\t//StringUtil.isNumer(pos) &&\n\t\t\t\t\t\t\tvs[i+1].startsWith(pos + \"/\") == false) {\n\t\t\t\t\t\tvs[i+1] = pos + \"/\" + vs[i+1];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn StringUtil.get(vs, \"]/\");\n\t\t\t}\n\t\t}\n\t\treturn valuePath;\n\t}\n\n\t//依赖引用关系 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**将已获取完成的object的内容替换requestObject里对应的值\n\t * @param path object的路径\n\t * @param result 需要被关联的object\n\t */\n\t@Override\n\tpublic void putQueryResult(String path, Object result) {\n\t\tLog.i(TAG, \"\\n putQueryResult  valuePath = \" + path + \"; result = \" + result + \"\\n <<<<<<<<<<<<<<<<<<<<<<<\");\n\t\t//\t\tif (queryResultMap.containsKey(valuePath)) {//只保存被关联的value\n\t\tLog.d(TAG, \"putQueryResult  queryResultMap.containsKey(valuePath) >> queryResultMap.put(path, result);\");\n\t\tqueryResultMap.put(path, result);\n\t\t//\t\t}\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48\n\t/**根据路径获取值\n\t * @param valuePath -the path need to get value\n\t * @return parent == null ? valuePath : parent.get(keys[keys.length - 1])\n\t * <p>use entrySet+getValue() to replace keySet+get() to enhance efficiency</p>\n\t */\n\t@Override\n\tpublic Object getValueByPath(String valuePath) {\n\t\tLog.i(TAG, \"<<<<<<<<<<<<<<< \\n getValueByPath  valuePath = \" + valuePath + \"\\n <<<<<<<<<<<<<<<<<<\");\n\t\tif (StringUtil.isEmpty(valuePath, true)) {\n\t\t\tLog.e(TAG, \"getValueByPath  StringUtil.isNotEmpty(valuePath, true) == false >> return null;\");\n\t\t\treturn null;\n\t\t}\n\t\tObject target = queryResultMap.get(valuePath);\n\t\tif (target != null) {\n\t\t\treturn target;\n\t\t}\n\n\t\t//取出key被valuePath包含的result，再从里面获取key对应的value\n\t\tObject parent = null;\n\t\tString[] keys = null;\n\t\tfor (Entry<String, Object> entry : queryResultMap.entrySet()){\n\t\t\tString path = entry.getKey();\n\t\t\tif (valuePath.startsWith(path + \"/\")) {\n\t\t\t\ttry {\n\t\t\t\t\tparent = entry.getValue();\n\t\t\t\t} catch (Exception e) {\n\t\t\t\t\tLog.e(TAG, \"getValueByPath  try { parent = (Map<String>) queryResultMap.get(path); } catch { \"\n\t\t\t\t\t\t\t+ \"\\n parent not instanceof Map<String>!\");\n\t\t\t\t\tparent = null;\n\t\t\t\t}\n\t\t\t\tif (parent != null) {\n\t\t\t\t\tkeys = StringUtil.splitPath(valuePath.substring(path.length()));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\ttarget = getValue(parent, keys); // 逐层到达targetKey的直接容器JSONObject parent\n\t\tif (target == null) { //从requestObject中取值\n\t\t\ttarget = getValue(requestObject, StringUtil.splitPath(valuePath));\n\t\t}\n\n\t\tLog.i(TAG, \"getValueByPath >> getValue >> return target = \" + target);\n\t\treturn target;\n\t}\n\n\t/**解码 引用赋值 路径中的 key，支持把 URL encode 后的值，转为 decode 后的原始值，例如 %2Fuser%2Flist -> /user/list ; %7B%7D -> []\n\t * @param key\n\t * @return\n\t */\n\tpublic static String getDecodedKey(String key) {\n\t\ttry {\n\t\t\treturn URLDecoder.decode(key, StringUtil.UTF_8);\n\t\t} catch (Throwable e) {\n\t\t\treturn key;\n\t\t}\n\t}\n\n\t//依赖引用关系 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\tpublic static final String KEY_CONFIG = \"config\";\n\n\tpublic static final String KEY_SQL = \"sql\";\n\n\tprotected Map<String, List<M>> arrayMainCacheMap = new HashMap<>();\n\tpublic void putArrayMainCache(String arrayPath, List<M> mainTableDataList) {\n\t\tarrayMainCacheMap.put(arrayPath, mainTableDataList);\n\t}\n\tpublic List<M> getArrayMainCache(String arrayPath) {\n\t\treturn arrayMainCacheMap.get(arrayPath);\n\t}\n\tpublic M getArrayMainCacheItem(String arrayPath, int position) {\n\t\tList<M> list = getArrayMainCache(arrayPath);\n\t\treturn list == null || position >= list.size() ? null : list.get(position);\n\t}\n\n\n\n\t/**执行 SQL 并返回 Map<String, Object>\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic M executeSQL(SQLConfig<T, M, L> config, boolean isSubquery) throws Exception {\n\t\tif (config == null) {\n\t\t\tLog.d(TAG, \"executeSQL  config == null >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\tconfig.setParser(this);\n\t\tconfig.setVersion(getVersion());\n\t\tconfig.setTag(getTag());\n\n\t\tif (isSubquery) {\n\t\t\tM sqlObj = JSON.createJSONObject();\n\t\t\tsqlObj.put(KEY_CONFIG, config);\n\t\t\treturn sqlObj;//容易丢失信息 JSON.parseObject(config);\n\t\t}\n\n\t\ttry {\n\t\t\tM result;\n\n\t\t\tboolean explain = config.isExplain();\n\t\t\tif (explain) {\n\t\t\t\t//如果先执行 explain，则 execute 会死循环，所以只能先执行非 explain\n\t\t\t\tconfig.setExplain(false); //对下面 config.getSQL(false); 生效\n\t\t\t\tM res = getSQLExecutor().execute(config, false);\n\n\t\t\t\t//如果是查询方法，才能执行explain\n\t\t\t\tif (RequestMethod.isQueryMethod(config.getMethod()) && config.isElasticsearch() == false){\n\t\t\t\t\tconfig.setExplain(explain);\n\t\t\t\t\tMap<String, Object> explainResult = config.isMain() && config.getPosition() != 0 ? null : getSQLExecutor().execute(config, false);\n\n\t\t\t\t\tif (explainResult == null) {\n\t\t\t\t\t\tresult = res;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tresult = JSON.createJSONObject();\n\t\t\t\t\t\tresult.put(KEY_EXPLAIN, explainResult);\n\t\t\t\t\t\tresult.putAll(res);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {//如果是更新请求，不执行explain，但可以返回sql\n\t\t\t\t\tresult = JSON.createJSONObject();\n\t\t\t\t\tresult.put(KEY_SQL, config.gainSQL(false));\n\t\t\t\t\tresult.putAll(res);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tresult = getSQLExecutor().execute(config, false);\n\t\t\t\t// FIXME 改为直接在 sqlExecutor 内加好，最后 Parser<T, M, L> 取结果，可以解决并发执行导致内部计算出错\n//\t\t\t\texecutedSQLDuration += sqlExecutor.getExecutedSQLDuration() + sqlExecutor.getSqlResultDuration();\n\t\t\t}\n\n\t\t\treturn result;\n\t\t}\n\t\tcatch (Exception e) {\n\t\t\tthrow CommonException.wrap(e, config);\n\t\t}\n\t\tfinally {\n\t\t\tif (config.getPosition() == 0 && config.limitSQLCount()) {\n\t\t\t\tint maxSQLCount = getMaxSQLCount();\n\t\t\t\tint sqlCount = getSQLExecutor().getExecutedSQLCount();\n\t\t\t\tLog.d(TAG, \"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< \\n\\n\\n 已执行 \" + sqlCount + \"/\" + maxSQLCount + \" 条 SQL \\n\\n\\n >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\");\n\t\t\t\tif (sqlCount > maxSQLCount) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"截至 \" + config.getTable() + \" 已执行 \" + sqlCount + \" 条 SQL，数量已超限，必须在 0-\" + maxSQLCount + \" 内 !\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\n\t//事务处理 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\tprivate int transactionIsolation = Connection.TRANSACTION_NONE;\n\t@Override\n\tpublic int getTransactionIsolation() {\n\t\treturn transactionIsolation;\n\t}\n\t@Override\n\tpublic void setTransactionIsolation(int transactionIsolation) {\n\t\tthis.transactionIsolation = transactionIsolation;\n\t}\n\n\t@Override\n\tpublic void begin(int transactionIsolation) {\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<<<<<<<<<<< begin transactionIsolation = \" + transactionIsolation + \" >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\n\t\tgetSQLExecutor().setTransactionIsolation(transactionIsolation); // 不知道 connection 什么时候创建，不能在这里准确控制，getSqlExecutor().begin(transactionIsolation);\n\t}\n\t@Override\n\tpublic void rollback() throws SQLException {\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<<<<<<<<<<< rollback >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\n\t\tgetSQLExecutor().rollback();\n\t}\n\t@Override\n\tpublic void rollback(Savepoint savepoint) throws SQLException {\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<<<<<<<<<<< rollback savepoint \" + (savepoint == null ? \"\" : \"!\") + \"= null >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\n\t\tgetSQLExecutor().rollback(savepoint);\n\t}\n\t@Override\n\tpublic void commit() throws SQLException {\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<<<<<<<<<<< commit >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\n\t\tgetSQLExecutor().commit();\n\t}\n\t@Override\n\tpublic void close() {\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<<<<<<<<<<< close >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\n\t\tgetSQLExecutor().close();\n\t}\n\n\t/**开始事务\n\t */\n\tprotected void onBegin() {\n\t\t//\t\tLog.d(TAG, \"onBegin >>\");\n\t\tif (RequestMethod.isQueryMethod(requestMethod)) {\n\t\t\treturn;\n\t\t}\n\n\t\tbegin(getTransactionIsolation());\n\t}\n\t/**提交事务\n\t */\n\tprotected void onCommit() {\n\t\t//\t\tLog.d(TAG, \"onCommit >>\");\n\t\t// this.sqlExecutor.getTransactionIsolation() 只有json第一次执行才会设置, get请求=0\n\t\tif (RequestMethod.isQueryMethod(requestMethod)\n\t\t\t\t&& getSQLExecutor().getTransactionIsolation() == Connection.TRANSACTION_NONE) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tcommit();\n\t\t}\n\t\tcatch (SQLException e) {\n\t\t\tLog.e(TAG, \"onCommit failed\", e);\n\t\t}\n\t}\n\t/**回滚事务\n\t */\n\tprotected void onRollback() {\n\t\t//\t\tLog.d(TAG, \"onRollback >>\");\n\t\tif (RequestMethod.isQueryMethod(requestMethod)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\trollback();\n\t\t}\n\t\tcatch (SQLException e1) {\n\t\t\tLog.e(TAG, \"onRollback failed\", e1);\n\t\t\ttry {\n\t\t\t\trollback(null);\n\t\t\t}\n\t\t\tcatch (SQLException e2) {\n\t\t\t\tLog.e(TAG, \"onRollback with null failed\", e2);\n\t\t\t}\n\t\t}\n\t}\n\n\t//事务处理 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\tprotected void onClose() {\n\t\t//\t\tLog.d(TAG, \"onClose >>\");\n\n\t\tclose();\n\t\tverifier = null;\n\t\tsqlExecutor = null;\n\t\tqueryResultMap.clear();\n\t\tqueryResultMap = null;\n\t}\n\n\tprivate void setOpMethod(Map<String, Object> request, ObjectParser<T, M, L> op, String key) {\n\t\tString _method = key == null ? null : getString(request, KEY_METHOD);\n\t\tif (_method != null) {\n\t\t\tRequestMethod method = RequestMethod.valueOf(_method); // 必须精准匹配，避免缓存命中率低\n\t\t\tthis.setMethod(method);\n\t\t\top.setMethod(method);\n\t\t}\n\t}\n\n\tprotected M getRequestStructure(RequestMethod method, String tag, int version) throws Exception {\n\t\t// 获取指定的JSON结构 <<<<<<<<<<<<\n\t\tM object = null;\n\t\tString error = \"\";\n\t\ttry {\n\t\t\tobject = getStructure(\"Request\", method.name(), tag, version);\n\t\t} catch (Exception e) {\n\t\t\terror = e.getMessage();\n\t\t}\n\t\tif (object == null) { // empty表示随意操作 || object.isEmpty()) {\n\t\t\tthrow new UnsupportedOperationException(\"找不到 version: \" + version + \", method: \" + method.name() + \", tag: \" + tag + \" 对应的 structure ！\" + \"非开放请求必须是后端 Request 表中校验规则允许的操作！\\n \" + error + \"\\n如果需要则在 Request 表中新增配置！\");\n\t\t}\n\n\t\treturn object;\n\t}\n\n\tpublic static final Map<String, RequestMethod> KEY_METHOD_ENUM_MAP;\n\tstatic {\n\t\tKEY_METHOD_ENUM_MAP = new LinkedHashMap<>();\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_GET, RequestMethod.GET);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_GETS, RequestMethod.GETS);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_HEAD, RequestMethod.HEAD);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_HEADS, RequestMethod.HEADS);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_POST, RequestMethod.POST);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_PUT, RequestMethod.PUT);\n\t\tKEY_METHOD_ENUM_MAP.put(KEY_DELETE, RequestMethod.DELETE);\n\t}\n\n\tprotected M batchVerify(RequestMethod method, String tag, int version, String name, @NotNull M request, int maxUpdateCount, SQLCreator<T, M, L> creator) throws Exception {\n\t\tM correctRequest = JSON.createJSONObject();\n\t\tList<String> removeTmpKeys = new ArrayList<>(); // 请求json里面的临时变量,不需要带入后面的业务中,比如 @post、@get等\n\n\t\tSet<String> reqSet = request == null ? null : request.keySet();\n\t\tif (reqSet == null || request.isEmpty()) {\n\t\t\tthrow new IllegalArgumentException(\"JSON 对象格式不正确 ！正确示例例如 \\\"User\\\": {}\");\n\t\t}\n\n\t\tfor (String key : reqSet) {\n\t\t\t// key 重复直接抛错(xxx:alias, xxx:alias[])\n\t\t\tif (correctRequest.containsKey(key) || correctRequest.containsKey(key + KEY_ARRAY)) {\n\t\t\t\tthrow new IllegalArgumentException(\"对象名重复,请添加别名区分 ! 重复对象名为: \" + key);\n\t\t\t}\n\n\t\t\tboolean isPost = KEY_POST.equals(key);\n\t\t\t// @post、@get 等 RequestMethod\n\t\t\ttry {\n\t\t\t\tRequestMethod keyMethod = isPost ? RequestMethod.POST : KEY_METHOD_ENUM_MAP.get(key);\n\t\t\t\tif (keyMethod != null) {\n\t\t\t\t\t// 如果不匹配,异常不处理即可\n\t\t\t\t\tremoveTmpKeys.add(key);\n\n\t\t\t\t\tObject val = request.get(key);\n\t\t\t\t\tMap<String, Object> obj = val instanceof Map<?, ?> ? JSON.get(request, key) : null;\n\t\t\t\t\tif (obj == null) {\n\t\t\t\t\t\tif (val instanceof String) {\n\t\t\t\t\t\t\tString[] tbls = StringUtil.split((String) val);\n\t\t\t\t\t\t\tif (tbls != null && tbls.length > 0) {\n\t\t\t\t\t\t\t\tobj = new LinkedHashMap<String, Object>();\n\t\t\t\t\t\t\t\tfor (int i = 0; i < tbls.length; i++) {\n\t\t\t\t\t\t\t\t\tString tbl = tbls[i];\n\t\t\t\t\t\t\t\t\tif (obj.containsKey(tbl)) {\n\t\t\t\t\t\t\t\t\t\tthrow new ConflictException(key + \": value 中 \" + tbl + \" 已经存在，不能重复！\");\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tobj.put(tbl, isPost && isTableArray(tbl)\n\t\t\t\t\t\t\t\t\t\t\t? tbl.substring(0, tbl.length() - 2) + \":[]\" : \"\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(key + \": value 中 value 类型错误，只能是 String 或 Map<String, Object> {} ！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tSet<Entry<String, Object>> set = obj == null ? new HashSet<>() : obj.entrySet();\n\n\t\t\t\t\tfor (Entry<String, Object> objEntry : set) {\n\t\t\t\t\t\tString objKey = objEntry == null ? null : objEntry.getKey();\n\t\t\t\t\t\tif (objKey == null) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tMap<String, Object> objAttrMap = new HashMap<>();\n\t\t\t\t\t\tobjAttrMap.put(KEY_METHOD, keyMethod);\n\t\t\t\t\t\tkeyObjectAttributesMap.put(objKey, objAttrMap);\n\n\t\t\t\t\t\tObject objVal = objEntry.getValue();\n\t\t\t\t\t\tMap<String, Object> objAttrJson = objVal instanceof Map<?, ?> ? JSON.getMap(obj, objKey) : null;\n\t\t\t\t\t\tif (objAttrJson == null) {\n\t\t\t\t\t\t\tif (objVal instanceof String) {\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_TAG, \"\".equals(objVal) ? objKey : objVal);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(key + \": { \" + objKey + \": value 中 value 类型错误，只能是 String 或 Map<String, Object> {} ！\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tSet<Entry<String, Object>> objSet = objAttrJson.entrySet();\n\n\t\t\t\t\t\t\tboolean hasTag = false;\n\t\t\t\t\t\t\tfor (Entry<String, Object> entry : objSet) {\n\t\t\t\t\t\t\t\tString objAttrKey = entry == null ? null : entry.getKey();\n\t\t\t\t\t\t\t\tif (objAttrKey == null) {\n\t\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tswitch (objAttrKey) {\n\t\t\t\t\t\t\t\t\tcase KEY_DATASOURCE:\n\t\t\t\t\t\t\t\t\tcase KEY_SCHEMA:\n\t\t\t\t\t\t\t\t\tcase KEY_DATABASE:\n\t\t\t\t\t\t\t\t\tcase KEY_VERSION:\n\t\t\t\t\t\t\t\t\tcase KEY_ROLE:\n\t\t\t\t\t\t\t\t\t\tobjAttrMap.put(objAttrKey, entry.getValue());\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\tcase KEY_TAG:\n\t\t\t\t\t\t\t\t\t\thasTag = true;\n\t\t\t\t\t\t\t\t\t\tobjAttrMap.put(objAttrKey, entry.getValue());\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (hasTag == false) {\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_TAG, isPost && isTableArray(objKey)\n\t\t\t\t\t\t\t\t\t\t? objKey.substring(0, objKey.length() - 2) + \":[]\" : objKey);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// 1、非crud,对于没有显式声明操作方法的，直接用 URL(/get, /post 等) 对应的默认操作方法\n\t\t\t\t// 2、crud, 没有声明就用 GET\n\t\t\t\t// 3、兼容 sql@ Map<String, Object>,设置 GET方法\n\t\t\t\t// 将method 设置到每个object, op执行会解析\n\t\t\t\tObject obj = request.get(key);\n\n\t\t\t\tif (obj instanceof Map<?, ?>) {\n\t\t\t\t\tMap<String, Object> attrMap = keyObjectAttributesMap.get(key);\n\n\t\t\t\t\tif (attrMap == null) {\n\t\t\t\t\t\t// 数组会解析为对象进行校验,做一下兼容\n\t\t\t\t\t\tif (keyObjectAttributesMap.get(key + KEY_ARRAY) == null) {\n\t\t\t\t\t\t\tif (method == RequestMethod.CRUD || key.endsWith(\"@\")) {\n\t\t\t\t\t\t\t\t((Map<String, Object>) obj).put(KEY_METHOD, GET);\n\t\t\t\t\t\t\t\tMap<String, Object> objAttrMap = new HashMap<>();\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_METHOD, GET);\n\t\t\t\t\t\t\t\tkeyObjectAttributesMap.put(key, objAttrMap);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t((Map<String, Object>) obj).put(KEY_METHOD, method);\n\t\t\t\t\t\t\t\tMap<String, Object> objAttrMap = new HashMap<>();\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_METHOD, method);\n\t\t\t\t\t\t\t\tkeyObjectAttributesMap.put(key, objAttrMap);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_METHOD, request);\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_DATASOURCE, request);\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_SCHEMA, request);\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_DATABASE, request);\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_VERSION, request);\n\t\t\t\t\t\t\tsetRequestAttribute(key, true, KEY_ROLE, request);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_METHOD, request);\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_DATASOURCE, request);\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_SCHEMA, request);\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_DATABASE, request);\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_VERSION, request);\n\t\t\t\t\t\tsetRequestAttribute(key, false, KEY_ROLE, request);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (key.startsWith(\"@\") || key.endsWith(\"@\")) {\n\t\t\t\t\tcorrectRequest.put(key, obj);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (obj instanceof Map<?, ?> || obj instanceof List<?>) {\n\t\t\t\t\tRequestMethod  _method;\n\t\t\t\t\tif (obj instanceof Map<?, ?>) {\n\t\t\t\t\t\tMap<String, Object> tblObj = JSON.getMap(request, key);\n\t\t\t\t\t\tString mn = tblObj == null ? null : getString(tblObj, KEY_METHOD);\n\t\t\t\t\t\t_method = mn == null ? null : RequestMethod.valueOf(mn);\n\t\t\t\t\t\tString combine = _method == null ? null : getString(tblObj, KEY_COMBINE);\n\t\t\t\t\t\tif (combine != null && RequestMethod.isPublicMethod(_method) == false) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(key + \":{} 里的 @combine:value 不合法！开放请求 GET、HEAD 才允许传 @combine:value !\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tMap<String, Object> attrMap = keyObjectAttributesMap.get(key);\n\n\t\t\t\t\t\tif (attrMap == null) {\n\t\t\t\t\t\t\tif (method == RequestMethod.CRUD) {\n\t\t\t\t\t\t\t\t_method = GET;\n\t\t\t\t\t\t\t\tMap<String, Object> objAttrMap = new HashMap<>();\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_METHOD, GET);\n\t\t\t\t\t\t\t\tkeyObjectAttributesMap.put(key, objAttrMap);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_method = method;\n\t\t\t\t\t\t\t\tMap<String, Object> objAttrMap = new HashMap<>();\n\t\t\t\t\t\t\t\tobjAttrMap.put(KEY_METHOD, method);\n\t\t\t\t\t\t\t\tkeyObjectAttributesMap.put(key, objAttrMap);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t_method = (RequestMethod) attrMap.get(KEY_METHOD);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// 非 CRUD 方法，都只能和 URL method 完全一致，避免意料之外的安全风险。\n\t\t\t\t\tif (method != RequestMethod.CRUD && _method != method) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"不支持在 \" + method + \" 中 \" + _method + \" ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// get请求不校验\n\t\t\t\t\tif (RequestMethod.isPublicMethod(_method)) {\n\t\t\t\t\t\tcorrectRequest.put(key, obj);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (tag != null && ! tag.contains(\":\")) {\n\t\t\t\t\t\tM object = getRequestStructure(_method, tag, version);\n\t\t\t\t\t\tM ret = objectVerify(_method, tag, version, name, request, maxUpdateCount, creator, object);\n\t\t\t\t\t\tcorrectRequest.putAll(ret);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tString _tag = buildTag(request, key, method, tag);\n\t\t\t\t\tM object = getRequestStructure(_method, _tag, version);\n\t\t\t\t\tif (method == RequestMethod.CRUD && StringUtil.isEmpty(tag, true)) {\n\t\t\t\t\t\tM requestItem = JSON.createJSONObject();\n\t\t\t\t\t\trequestItem.put(key, obj);\n\t\t\t\t\t\tMap<String, Object> ret = objectVerify(_method, _tag, version, name, requestItem, maxUpdateCount, creator, object);\n\t\t\t\t\t\tcorrectRequest.put(key, ret.get(key));\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn objectVerify(_method, _tag, version, name, request, maxUpdateCount, creator, object);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcorrectRequest.put(key, obj);\n\t\t\t\t}\n\t\t\t} catch (Exception e) {\n\t\t\t\tLog.e(TAG, \"parseCorrectRequest failed\", e);\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\n\t\t// 这里是 requestObject ref request 的引用, 删除不需要的临时变量\n\t\tfor (String removeKey : removeTmpKeys) {\n\t\t\trequest.remove(removeKey);\n\t\t}\n\n\t\treturn correctRequest;\n\t}\n\n\tpublic static <E extends Enum<E>> E getEnum(final Class<E> enumClass, final String enumName, final E defaultEnum) {\n        if (enumName == null) {\n            return defaultEnum;\n        }\n        try {\n            return Enum.valueOf(enumClass, enumName);\n        } catch (final IllegalArgumentException ex) {\n            return defaultEnum;\n        }\n    }\n\n\tprotected void setRequestAttribute(String key, boolean isArray, String attrKey, @NotNull Map<String, Object> request) {\n\t\tMap<String, Object> attrMap = keyObjectAttributesMap.get(isArray ? key + KEY_ARRAY : key);\n\t\tObject attrVal = attrMap == null ? null : attrMap.get(attrKey);\n\t\tMap<String, Object> obj = attrVal == null ? null : JSON.get(request, key);\n\n\t\tif (obj != null && obj.get(attrKey) == null) {\n\t\t\t// 如果对象内部已经包含该属性,不覆盖\n\t\t\tobj.put(attrKey, attrVal);\n\t\t}\n\t}\n\n\tprotected String buildTag(Map<String, Object> request, String key, RequestMethod method, String tag) {\n\t\tif (method == RequestMethod.CRUD) {\n\t\t\tMap<String, Object> attrMap = keyObjectAttributesMap.get(key);\n\t\t\tObject _tag = attrMap == null ? null : attrMap.get(KEY_TAG);\n\t\t\treturn _tag != null ? _tag.toString() : StringUtil.isEmpty(tag) ? key : tag;\n\t\t} else {\n\t\t\tif (StringUtil.isEmpty(tag, true)) {\n\t\t\t\tthrow new IllegalArgumentException(\"请在最外层传 tag ！一般是 Table 名，例如 \\\"tag\\\": \\\"User\\\" \");\n\t\t\t}\n\t\t}\n\t\treturn tag;\n\t}\n\n\n\tprotected M objectVerify(RequestMethod method, String tag, int version, String name, @NotNull M request\n\t\t\t, int maxUpdateCount, SQLCreator<T, M, L> creator, M object) throws Exception {\n\t\t// 获取指定的JSON结构 >>>>>>>>>>>>>>\n\t\tM target = wrapRequest(method, tag, object, true);\n\t\t// Map<String, Object> clone 浅拷贝没用，Structure.parse 会导致 structure 里面被清空，第二次从缓存里取到的就是 {}\n\t\treturn getVerifier().setParser(this).verifyRequest(method, name, target, request, maxUpdateCount, getGlobalDatabase(), getGlobalSchema());\n\t}\n\n\t/***\n\t * 兼容url crud, 获取真实method\n\t * @param method = crud\n\t * @param key\n\t * @return\n\t */\n\tpublic RequestMethod getRealMethod(RequestMethod method, String key, Object value) {\n\t\tif (method == CRUD && (value instanceof Map<?, ?> || value instanceof List<?>)) {\n\t\t\tMap<String, Object> attrMap = keyObjectAttributesMap.get(key);\n\t\t\tObject _method = attrMap == null ? null : attrMap.get(KEY_METHOD);\n\t\t\tif (_method instanceof RequestMethod) {\n\t\t\t\treturn (RequestMethod) _method;\n\t\t\t}\n\t\t}\n\n\t\treturn method;\n\t}\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.*;\nimport apijson.orm.Join.On;\nimport apijson.orm.exception.NotExistException;\nimport apijson.orm.exception.UnsupportedDataTypeException;\nimport apijson.orm.model.*;\n\nimport java.util.*;\nimport java.util.Map.Entry;\nimport java.util.regex.Pattern;\n\nimport static apijson.JSON.getBoolean;\nimport static apijson.JSON.getString;\nimport static apijson.JSONMap.*;\nimport static apijson.RequestMethod.*;\nimport static apijson.SQL.*;\n\n/**config sql for JSON Request\n * @author Lemon\n */\npublic abstract class AbstractSQLConfig<T, M extends Map<String, Object>, L extends List<Object>>\n\t\timplements SQLConfig<T, M, L> {\n\tprivate static final String TAG = \"AbstractSQLConfig\";\n\n\t/**\n\t * 为 true 则兼容 5.0 之前 @having:\"toId>0;avg(id)<100000\" 默认 AND 连接，为 HAVING toId>0 AND avg(id)<100000；\n\t * 否则按 5.0+ 新版默认 OR 连接，为 HAVING toId>0 OR avg(id)<100000，使用 @having& 或 @having:{ @combine: null } 时才用 AND 连接\n\t */\n\tpublic static boolean IS_HAVING_DEFAULT_AND = false;\n\t/**\n\t * 为 true 则兼容 5.0 之前 @having:\"toId>0\" 这种不包含 SQL 函数的表达式；\n\t * 否则按 5.0+ 新版不允许，可以用 @having:\"(toId)>0\" 替代\n\t */\n\tpublic static boolean IS_HAVING_ALLOW_NOT_FUNCTION = false;\n\n\t/**\n\t * 开启 WITH AS 表达式(在支持这种语法的数据库及版本)来简化 SQL 和提升性能\n\t */\n\tpublic static boolean ENABLE_WITH_AS = false;\n\n\t/**\n\t * 对指定的方法，忽略空字符串，不作为 GET 条件，PUT 值等。可取值 new RequestMethod[]{ RequestMethod.GET, RequestMethod.POST ... }\n\t */\n\tpublic static List<RequestMethod> IGNORE_EMPTY_STRING_METHOD_LIST = null;\n\t/**\n\t * 对指定的方法，忽略空白字符串。即首尾 trim 去掉所有不可见字符后，仍然为空的，就忽略，不作为 GET 条件，PUT 值等。\n\t * 可取值 new RequestMethod[]{ RequestMethod.GET, RequestMethod.POST ... }\n\t */\n\tpublic static List<RequestMethod> IGNORE_BLANK_STRING_METHOD_LIST = null;\n\n\tpublic static String KEY_DELETED_KEY = \"deletedKey\";\n\tpublic static String KEY_DELETED_VALUE = \"deletedValue\";\n\tpublic static String KEY_NOT_DELETED_VALUE = \"notDeletedValue\";\n\n\tpublic static int MAX_HAVING_COUNT = 5;\n\tpublic static int MAX_WHERE_COUNT = 10;\n\tpublic static int MAX_COMBINE_DEPTH = 2;\n\tpublic static int MAX_COMBINE_COUNT = 5;\n\tpublic static int MAX_COMBINE_KEY_COUNT = 2;\n\tpublic static float MAX_COMBINE_RATIO = 1.0f;\n\tpublic static boolean ALLOW_MISSING_KEY_4_COMBINE = true;\n\n\tpublic static String DEFAULT_DATABASE = DATABASE_MYSQL;\n\tpublic static String DEFAULT_NAMESPACE = \"\"; // \"root\";\n\tpublic static String DEFAULT_CATALOG = \"\"; // PostgreSQL JDBC 必须在 URI 中传 \"\"postgres\";\n\tpublic static String DEFAULT_SCHEMA = \"sys\";\n\tpublic static String PREFIX_DISTINCT = \"DISTINCT \";\n\n\tpublic static Pattern PATTERN_SCHEMA;\n\t// * 和 / 不能同时出现，防止 /* */ 段注释！ # 和 -- 不能出现，防止行注释！ ; 不能出现，防止隔断SQL语句！空格不能出现，防止 CRUD,DROP,SHOW TABLES等语句！\n\tpublic static Pattern PATTERN_RANGE;\n\tpublic static Pattern PATTERN_FUNCTION;\n\n\t/**\n\t * 表 SCHEMA 映射\n\t */\n\tpublic static Map<String, String> TABLE_SCHEMA_MAP;\n\n\t/**\n\t * 表名映射，隐藏真实表名，对安全要求很高的表可以这么做\n\t */\n\tpublic static Map<String, String> TABLE_KEY_MAP;\n\t/**\n\t * 字段名映射，隐藏真实字段名，对安全要求很高的表可以这么做，另外可以配置 name_tag:(name,tag) 来实现多字段 IN，length_tag:length(tag) 来实现 SQL 函数复杂条件\n\t */\n\tpublic static Map<String, String> COLUMN_KEY_MAP;\n\t/**\n\t * 允许批量增删改部分记录失败的表\n\t */\n\tpublic static Map<String, String> ALLOW_PARTIAL_UPDATE_FAIL_TABLE_MAP;\n\tpublic static List<String> CONFIG_TABLE_LIST;\n\tpublic static List<String> DATABASE_LIST;\n\n\t// 自定义原始 SQL 片段 Map<key, substring>：当 substring 为 null 时忽略；当 substring 为 \"\" 时整个 value 是 raw SQL；其它情况则只是 substring 这段为 raw SQL\n\tpublic static Map<String, String> RAW_MAP;\n\t// 允许调用的 SQL 函数：当 substring 为 null 时忽略；当 substring 为 \"\" 时整个 value 是 raw SQL；其它情况则只是 substring 这段为 raw SQL\n\tpublic static Map<String, String> SQL_AGGREGATE_FUNCTION_MAP;\n\tpublic static Map<String, String> SQL_FUNCTION_MAP;\n\n\tstatic {  // 凡是 SQL 边界符、分隔符、注释符 都不允许，例如 ' \" ` ( ) ; # -- /**/ ，以免拼接 SQL 时被注入意外可执行指令\n\t\tPATTERN_SCHEMA = Pattern.compile(\"^[A-Za-z0-9_-]+$\");\n\t\tPATTERN_RANGE = Pattern.compile(\"^[0-9%,!=\\\\<\\\\>/\\\\.\\\\+\\\\-\\\\*\\\\^]+$\"); // ^[a-zA-Z0-9_*%!=<>(),\"]+$ 导致 exists(select*from(Comment)) 通过！\n\t\t// TODO 改成更好的正则，校验前面为单词，中间为操作符，后面为值\n\t\tPATTERN_FUNCTION = Pattern.compile(\"^[A-Za-z0-9%,:_@&~`!=\\\\<\\\\>\\\\|\\\\[\\\\]\\\\{\\\\} /\\\\.\\\\+\\\\-\\\\*\\\\^\\\\?\\\\(\\\\)\\\\$]+$\");\n\n\t\tTABLE_SCHEMA_MAP = new HashMap<>();\n\t\tTABLE_SCHEMA_MAP.put(Table.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(Column.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(PgClass.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(PgAttribute.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(SysTable.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(SysColumn.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(ExtendedProperty.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(AllTable.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(AllColumn.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(AllTableComment.class.getSimpleName(), DEFAULT_SCHEMA);\n\t\tTABLE_SCHEMA_MAP.put(AllColumnComment.class.getSimpleName(), DEFAULT_SCHEMA);\n\n\t\tTABLE_KEY_MAP = new HashMap<>();\n\t\tTABLE_KEY_MAP.put(Table.class.getSimpleName(), Table.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(Column.class.getSimpleName(), Column.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(PgClass.class.getSimpleName(), PgClass.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(PgAttribute.class.getSimpleName(), PgAttribute.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(SysTable.class.getSimpleName(), SysTable.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(SysColumn.class.getSimpleName(), SysColumn.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(ExtendedProperty.class.getSimpleName(), ExtendedProperty.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(AllTable.class.getSimpleName(), AllTable.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(AllColumn.class.getSimpleName(), AllColumn.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(AllTableComment.class.getSimpleName(), AllTableComment.TABLE_NAME);\n\t\tTABLE_KEY_MAP.put(AllColumnComment.class.getSimpleName(), AllColumnComment.TABLE_NAME);\n\n\t\tALLOW_PARTIAL_UPDATE_FAIL_TABLE_MAP = new HashMap<>();\n\n\t\tCONFIG_TABLE_LIST = new ArrayList<>();  // Table, Column 等是系统表 AbstractVerifier.SYSTEM_ACCESS_MAP.keySet());\n\t\tCONFIG_TABLE_LIST.add(Function.class.getSimpleName());\n\t\tCONFIG_TABLE_LIST.add(Request.class.getSimpleName());\n\t\tCONFIG_TABLE_LIST.add(Access.class.getSimpleName());\n\t\tCONFIG_TABLE_LIST.add(Document.class.getSimpleName());\n\t\tCONFIG_TABLE_LIST.add(TestRecord.class.getSimpleName());\n\n\n\t\tDATABASE_LIST = new ArrayList<>();\n\t\tDATABASE_LIST.add(DATABASE_MYSQL);\n\t\tDATABASE_LIST.add(DATABASE_POSTGRESQL);\n\t\tDATABASE_LIST.add(DATABASE_SQLSERVER);\n\t\tDATABASE_LIST.add(DATABASE_ORACLE);\n\t\tDATABASE_LIST.add(DATABASE_DB2);\n\t\tDATABASE_LIST.add(DATABASE_MARIADB);\n\t\tDATABASE_LIST.add(DATABASE_TIDB);\n\t\tDATABASE_LIST.add(DATABASE_COCKROACHDB);\n\t\tDATABASE_LIST.add(DATABASE_DAMENG);\n\t\tDATABASE_LIST.add(DATABASE_KINGBASE);\n\t\tDATABASE_LIST.add(DATABASE_ELASTICSEARCH);\n\t\tDATABASE_LIST.add(DATABASE_MANTICORE);\n\t\tDATABASE_LIST.add(DATABASE_CLICKHOUSE);\n\t\tDATABASE_LIST.add(DATABASE_HIVE);\n\t\tDATABASE_LIST.add(DATABASE_PRESTO);\n\t\tDATABASE_LIST.add(DATABASE_TRINO);\n\t\tDATABASE_LIST.add(DATABASE_MILVUS);\n\t\tDATABASE_LIST.add(DATABASE_INFLUXDB);\n\t\tDATABASE_LIST.add(DATABASE_TDENGINE);\n\t\tDATABASE_LIST.add(DATABASE_TIMESCALEDB);\n\t\tDATABASE_LIST.add(DATABASE_QUESTDB);\n\t\tDATABASE_LIST.add(DATABASE_IOTDB);\n\t\tDATABASE_LIST.add(DATABASE_SNOWFLAKE);\n\t\tDATABASE_LIST.add(DATABASE_DATABEND);\n\t\tDATABASE_LIST.add(DATABASE_DATABRICKS);\n\t\tDATABASE_LIST.add(DATABASE_REDIS);\n\t\tDATABASE_LIST.add(DATABASE_MONGODB);\n\t\tDATABASE_LIST.add(DATABASE_CASSANDRA);\n\t\tDATABASE_LIST.add(DATABASE_KAFKA);\n\t\tDATABASE_LIST.add(DATABASE_MQ);\n\t\tDATABASE_LIST.add(DATABASE_DUCKDB);\n\t\tDATABASE_LIST.add(DATABASE_SURREALDB);\n\t\tDATABASE_LIST.add(DATABASE_OPENGAUSS);\n\t\tDATABASE_LIST.add(DATABASE_DORIS);\n\n\n\t\tRAW_MAP = new LinkedHashMap<>();  // 保证顺序，避免配置冲突等意外情况\n\n\t\tRAW_MAP.put(\"+\", \"\");\n\t\tRAW_MAP.put(\"-\", \"\");\n\t\tRAW_MAP.put(\"*\", \"\");\n\t\tRAW_MAP.put(\"/\", \"\");\n\t\tRAW_MAP.put(\"=\", \"\");\n\t\tRAW_MAP.put(\"!=\", \"\");\n\t\tRAW_MAP.put(\">\", \"\");\n\t\tRAW_MAP.put(\">=\", \"\");\n\t\tRAW_MAP.put(\"<\", \"\");\n\t\tRAW_MAP.put(\"<=\", \"\");\n\t\tRAW_MAP.put(\"%\", \"\");\n\t\tRAW_MAP.put(\"(\", \"\");\n\t\tRAW_MAP.put(\")\", \"\");\n\n\t\tRAW_MAP.put(\"&\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"|\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"^\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"~\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"&=\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"|=\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"~=\", \"\"); // 位运算\n\t\tRAW_MAP.put(\">>\", \"\"); // 位运算\n\t\tRAW_MAP.put(\"<<\", \"\"); // 位运算\n\n\t\t// MySQL 关键字\n\t\tRAW_MAP.put(\"AS\", \"\");\n\t\tRAW_MAP.put(\"IS NOT NULL\", \"\");\n\t\tRAW_MAP.put(\"IS NULL\", \"\");\n\t\tRAW_MAP.put(\"IS\", \"\");\n\t\tRAW_MAP.put(\"NULL\", \"\");\n\t\tRAW_MAP.put(\"AND\", \"\");\n\t\tRAW_MAP.put(\"OR\", \"\");\n\t\tRAW_MAP.put(\"NOT\", \"\");\n\t\tRAW_MAP.put(\"VALUE\", \"\");\n\t\tRAW_MAP.put(\"DISTINCT\", \"\");\n\t\tRAW_MAP.put(\"CASE\", \"\");\n\t\tRAW_MAP.put(\"WHEN\", \"\");\n\t\tRAW_MAP.put(\"THEN\", \"\");\n\t\tRAW_MAP.put(\"ELSE\", \"\");\n\t\tRAW_MAP.put(\"END\", \"\");\n\n\t\t//时间\n\t\tRAW_MAP.put(\"now()\", \"\");\n\t\tRAW_MAP.put(\"DATE\", \"\");\n\t\tRAW_MAP.put(\"TIME\", \"\");\n\t\tRAW_MAP.put(\"DATETIME\", \"\");\n\t\tRAW_MAP.put(\"TIMESTAMP\", \"\");\n\t\tRAW_MAP.put(\"DateTime\", \"\");\n\t\tRAW_MAP.put(\"SECOND\", \"\");\n\t\tRAW_MAP.put(\"MINUTE\", \"\");\n\t\tRAW_MAP.put(\"HOUR\", \"\");\n\t\tRAW_MAP.put(\"DAY\", \"\");\n\t\tRAW_MAP.put(\"WEEK\", \"\");\n\t\tRAW_MAP.put(\"MONTH\", \"\");\n\t\tRAW_MAP.put(\"QUARTER\", \"\");\n\t\tRAW_MAP.put(\"YEAR\", \"\");\n\t\t//\t\tRAW_MAP.put(\"json\", \"\");\n\t\t//\t\tRAW_MAP.put(\"unit\", \"\");\n\n\t\t//MYSQL 数据类型 BINARY，CHAR，DATETIME，TIME，DECIMAL，SIGNED，UNSIGNED\n\t\tRAW_MAP.put(\"BINARY\", \"\");\n\t\tRAW_MAP.put(\"SIGNED\", \"\");\n\t\tRAW_MAP.put(\"DECIMAL\", \"\");\n\t\tRAW_MAP.put(\"DOUBLE\", \"\");\n\t\tRAW_MAP.put(\"FLOAT\", \"\");\n\t\tRAW_MAP.put(\"BOOLEAN\", \"\");\n\t\tRAW_MAP.put(\"ENUM\", \"\");\n\t\tRAW_MAP.put(\"SET\", \"\");\n\t\tRAW_MAP.put(\"POINT\", \"\");\n\t\tRAW_MAP.put(\"BLOB\", \"\");\n\t\tRAW_MAP.put(\"LONGBLOB\", \"\");\n\t\tRAW_MAP.put(\"UNSIGNED\", \"\");\n\t\tRAW_MAP.put(\"BIT\", \"\");\n\t\tRAW_MAP.put(\"TINYINT\", \"\");\n\t\tRAW_MAP.put(\"SMALLINT\", \"\");\n\t\tRAW_MAP.put(\"INT\", \"\");\n\t\tRAW_MAP.put(\"BIGINT\", \"\");\n\t\tRAW_MAP.put(\"CHAR\", \"\");\n\t\tRAW_MAP.put(\"VARCHAR\", \"\");\n\t\tRAW_MAP.put(\"TEXT\", \"\");\n\t\tRAW_MAP.put(\"LONGTEXT\", \"\");\n\t\tRAW_MAP.put(\"JSON\", \"\");\n\n\t\t//窗口函数关键字\n\t\tRAW_MAP.put(\"OVER\", \"\");\n\t\tRAW_MAP.put(\"INTERVAL\", \"\");\n\t\tRAW_MAP.put(\"GROUP BY\", \"\"); // 往前\n\t\tRAW_MAP.put(\"GROUP\", \"\"); // 往前\n\t\tRAW_MAP.put(\"ORDER BY\", \"\"); // 往前\n\t\tRAW_MAP.put(\"ORDER\", \"\");\n\t\tRAW_MAP.put(\"PARTITION BY\", \"\"); // 往前\n\t\tRAW_MAP.put(\"PARTITION\", \"\"); // 往前\n\t\tRAW_MAP.put(\"BY\", \"\");\n\t\tRAW_MAP.put(\"DESC\", \"\");\n\t\tRAW_MAP.put(\"ASC\", \"\");\n\t\tRAW_MAP.put(\"PRECEDING\", \"\"); // 往前\n\t\tRAW_MAP.put(\"FOLLOWING\", \"\"); // 往后\n\t\tRAW_MAP.put(\"BETWEEN\", \"\");\n\t\tRAW_MAP.put(\"ROWS\", \"\");\n\n\t\tRAW_MAP.put(\"AGAINST\", \"\");\n\t\tRAW_MAP.put(\"IN NATURAL LANGUAGE MODE\", \"\");\n\t\tRAW_MAP.put(\"IN BOOLEAN MODE\", \"\");\n\t\tRAW_MAP.put(\"IN\", \"\");\n\t\tRAW_MAP.put(\"NATURAL\", \"\");\n\t\tRAW_MAP.put(\"LANGUAGE\", \"\");\n\t\tRAW_MAP.put(\"MODE\", \"\");\n\n\n\t\tSQL_AGGREGATE_FUNCTION_MAP = new LinkedHashMap<>();  // 保证顺序，避免配置冲突等意外情况\n\t\tSQL_AGGREGATE_FUNCTION_MAP.put(\"max\", \"\"); // MAX(a, b, c ...) 最大值\n\t\tSQL_AGGREGATE_FUNCTION_MAP.put(\"min\", \"\"); // MIN(a, b, c ...) 最小值\n\t\tSQL_AGGREGATE_FUNCTION_MAP.put(\"avg\", \"\"); // AVG(a, b, c ...) 平均值\n\t\tSQL_AGGREGATE_FUNCTION_MAP.put(\"count\", \"\"); // COUNT(a, b, c ...) 总数\n\t\tSQL_AGGREGATE_FUNCTION_MAP.put(\"sum\", \"\"); // SUM(a, b, c ...) 总和\n\n\t\tSQL_FUNCTION_MAP = new LinkedHashMap<>(); // 保证顺序，避免配置冲突等意外情况\n\n\t\t// 窗口函数\n\t\tSQL_FUNCTION_MAP.put(\"rank\", \"\"); // RANK(a, b, c ...) 得到数据项在分组中的排名，排名相等的时候会留下空位\n\t\tSQL_FUNCTION_MAP.put(\"dense_rank\", \"\"); // DENSE_RANK(a, b, c ...) 得到数据项在分组中的排名，排名相等的时候不会留下空位\n\t\tSQL_FUNCTION_MAP.put(\"row_num\", \"\"); // ROW_NUM() 按照分组中的顺序生成序列，不存在重复的序列\n\t\tSQL_FUNCTION_MAP.put(\"row_number\", \"\"); // ROW_NUMBER() 按照分组中的顺序生成序列，不存在重复的序列\n\t\tSQL_FUNCTION_MAP.put(\"ntile\", \"\"); // NTILE(a, b, c ...) 用于将分组数据按照顺序切分成N片，返回当前切片值，不支持ROWS_BETWEE\n\t\tSQL_FUNCTION_MAP.put(\"first_value\", \"\"); // FIRST_VALUE() 取分组排序后，截止到当前行，分组内第一个值\n\t\tSQL_FUNCTION_MAP.put(\"last_value\", \"\"); // LAST_VALUE() 取分组排序后，截止到当前行，分组内的最后一个值\n\t\tSQL_FUNCTION_MAP.put(\"lag\", \"\"); // LAG() 统计窗口内往上第n行值。第一个参数为列名，第二个参数为往上第n行（可选，默认为1），第三个参数为默认值（当往上第n行为NULL时候，取默认值，如不指定，则为NULL）\n\t\tSQL_FUNCTION_MAP.put(\"lead\", \"\"); // LEAD() 统计窗口内往下第n行值。第一个参数为列名，第二个参数为往下第n行（可选，默认为1），第三个参数为默认值（当往下第n行为NULL时候，取默认值，如不指定，则为NULL）\n\t\tSQL_FUNCTION_MAP.put(\"cume_dist\", \"\"); // CUME_DIST() 返回（小于等于当前行值的行数）/（当前分组内的总行数）\n\t\tSQL_FUNCTION_MAP.put(\"percent_rank\", \"\"); // PERCENT_RANK(a, b, c ...) 返回（组内当前行的rank值-1）/（分组内做总行数-1）\n\n\t\t// MySQL 字符串函数\n\t\tSQL_FUNCTION_MAP.put(\"ascii\", \"\"); // ASCII(s)\t返回字符串 s 的第一个字符的 ASCII 码。\n\t\tSQL_FUNCTION_MAP.put(\"char_length\", \"\"); // CHAR_LENGTH(s)\t返回字符串 s 的字符数\n\t\tSQL_FUNCTION_MAP.put(\"character_length\", \"\"); // CHARACTER_LENGTH(s)\t返回字符串 s 的字符数\n\t\tSQL_FUNCTION_MAP.put(\"concat\", \"\"); // CONCAT(s1, s2...sn)\t字符串 s1,s2 等多个字符串合并为一个字符串\n\t\tSQL_FUNCTION_MAP.put(\"concat_ws\", \"\"); // CONCAT_WS(x, s1, s2...sn)\t同 CONCAT(s1, s2 ...) 函数，但是每个字符串之间要加上 x，x 可以是分隔符\n\t\tSQL_FUNCTION_MAP.put(\"field\", \"\"); // FIELD(s, s1, s2...)\t返回第一个字符串 s 在字符串列表 (s1, s2...)中的位置\n\t\tSQL_FUNCTION_MAP.put(\"find_in_set\", \"\"); // FIND_IN_SET(s1, s2)\t返回在字符串s2中与s1匹配的字符串的位置\n\t\tSQL_FUNCTION_MAP.put(\"format\", \"\"); // FORMAT(x, n)\t函数可以将数字 x 进行格式化 \"#,###.##\", 将 x 保留到小数点后 n 位，最后一位四舍五入。\n\t\tSQL_FUNCTION_MAP.put(\"insert\", \"\"); // INSERT(s1, x, len, s2)\t字符串 s2 替换 s1 的 x 位置开始长度为 len 的字符串\n\t\tSQL_FUNCTION_MAP.put(\"locate\", \"\"); // LOCATE(s1, s)\t从字符串 s 中获取 s1 的开始位置\n\t\tSQL_FUNCTION_MAP.put(\"lcase\", \"\"); // LCASE(s)\t将字符串 s 的所有字母变成小写字母\n\t\tSQL_FUNCTION_MAP.put(\"left\", \"\"); // LEFT(s, n)\t返回字符串 s 的前 n 个字符\n\t\tSQL_FUNCTION_MAP.put(\"length\", \"\"); // LENGTH(s)\t返回字符串 s 的字符数\n\t\tSQL_FUNCTION_MAP.put(\"lower\", \"\"); // LOWER(s)\t将字符串 s 的所有字母变成小写字母\n\t\tSQL_FUNCTION_MAP.put(\"lpad\", \"\"); // LPAD(s1, len, s2)\t在字符串 s1 的开始处填充字符串 s2，使字符串长度达到 len\n\t\tSQL_FUNCTION_MAP.put(\"ltrim\", \"\"); // LTRIM(s)\t去掉字符串 s 开始处的空格\n\t\tSQL_FUNCTION_MAP.put(\"mid\", \"\"); // MID(s, n, len)\t从字符串 s 的 n 位置截取长度为 len 的子字符串，同 SUBSTRING(s, n, len)\n\t\tSQL_FUNCTION_MAP.put(\"position\", \"\"); // POSITION(s, s1);\t从字符串 s 中获取 s1 的开始位置\n\t\tSQL_FUNCTION_MAP.put(\"repeat\", \"\"); // REPEAT(s, n)\t将字符串 s 重复 n 次\n\t\tSQL_FUNCTION_MAP.put(\"replace\", \"\"); // REPLACE(s, s1, s2)\t将字符串 s2 替代字符串 s 中的字符串 s1\n\t\tSQL_FUNCTION_MAP.put(\"reverse\", \"\"); // REVERSE(s); // )\t将字符串s的顺序反过来\n\t\tSQL_FUNCTION_MAP.put(\"right\", \"\"); // RIGHT(s, n)\t返回字符串 s 的后 n 个字符\n\t\tSQL_FUNCTION_MAP.put(\"rpad\", \"\"); // RPAD(s1, len, s2)\t在字符串 s1 的结尾处添加字符串 s2，使字符串的长度达到 len\n\t\tSQL_FUNCTION_MAP.put(\"rtrim\", \"\"); // RTRIM\", \"\"); // )\t去掉字符串 s 结尾处的空格\n\t\tSQL_FUNCTION_MAP.put(\"space\", \"\"); // SPACE(n)\t返回 n 个空格\n\t\tSQL_FUNCTION_MAP.put(\"strcmp\", \"\"); // STRCMP(s1, s2)\t比较字符串 s1 和 s2，如果 s1 与 s2 相等返回 0 ，如果 s1>s2 返回 1，如果 s1<s2 返回 -1\n\t\tSQL_FUNCTION_MAP.put(\"substr\", \"\"); // SUBSTR(s, start, length)\t从字符串 s 的 start 位置截取长度为 length 的子字符串\n\t\tSQL_FUNCTION_MAP.put(\"substring\", \"\"); // STRING(s, start, length))\t从字符串 s 的 start 位置截取长度为 length 的子字符串\n\t\tSQL_FUNCTION_MAP.put(\"substring_index\", \"\"); // SUBSTRING_INDEX(s, delimiter, number)\t返回从字符串 s 的第 number 个出现的分隔符 delimiter 之后的子串。\n\t\tSQL_FUNCTION_MAP.put(\"trim\", \"\"); // TRIM(s)\t去掉字符串 s 开始和结尾处的空格\n\t\tSQL_FUNCTION_MAP.put(\"ucase\", \"\"); // UCASE(s)\t将字符串转换为大写\n\t\tSQL_FUNCTION_MAP.put(\"upper\", \"\"); // UPPER(s)\t将字符串转换为大写\n\n\t\t// MySQL 数字函数\n\t\tSQL_FUNCTION_MAP.put(\"abs\", \"\"); // ABS(x)\t返回 x 的绝对值　　\n\t\tSQL_FUNCTION_MAP.put(\"acos\", \"\"); // ACOS(x)\t求 x 的反余弦值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"asin\", \"\"); // ASIN(x)\t求反正弦值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"atan\", \"\"); // ATAN(x)\t求反正切值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"atan2\", \"\"); // ATAN2(n, m)\t求反正切值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"avg\", \"\"); // AVG(expression)\t返回一个表达式的平均值，expression 是一个字段\n\t\tSQL_FUNCTION_MAP.put(\"ceil\", \"\"); // CEIL(x)\t返回大于或等于 x 的最小整数　\n\t\tSQL_FUNCTION_MAP.put(\"ceiling\", \"\"); // CEILING(x)\t返回大于或等于 x 的最小整数　\n\t\tSQL_FUNCTION_MAP.put(\"cos\", \"\"); // COS(x)\t求余弦值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"cot\", \"\"); // COT(x)\t求余切值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"count\", \"\"); // COUNT(expression)\t返回查询的记录总数，expression 参数是一个字段或者 * 号\n\t\tSQL_FUNCTION_MAP.put(\"degrees\", \"\"); // DEGREES(x)\t将弧度转换为角度　　\n\t\tSQL_FUNCTION_MAP.put(\"div\", \"\"); // n DIV m\t整除，n 为被除数，m 为除数\n\t\tSQL_FUNCTION_MAP.put(\"exp\", \"\"); // EXP(x)\t返回 e 的 x 次方　　\n\t\tSQL_FUNCTION_MAP.put(\"floor\", \"\"); // FLOOR(x)\t返回小于或等于 x 的最大整数　　\n\t\tSQL_FUNCTION_MAP.put(\"greatest\", \"\"); // GREATEST(expr1, expr2, expr3, ...)\t返回列表中的最大值\n\t\tSQL_FUNCTION_MAP.put(\"least\", \"\"); // LEAST(expr1, expr2, expr3, ...)\t返回列表中的最小值\n\t\tSQL_FUNCTION_MAP.put(\"ln\", \"\"); // 2);  LN\t返回数字的自然对数，以 e 为底。\n\t\tSQL_FUNCTION_MAP.put(\"log\", \"\"); // LOG(x) 或 LOG(base, x)\t返回自然对数(以 e 为底的对数)，如果带有 base 参数，则 base 为指定带底数。　　\n\t\tSQL_FUNCTION_MAP.put(\"log10\", \"\"); // LOG10(x)\t返回以 10 为底的对数　　\n\t\tSQL_FUNCTION_MAP.put(\"log2\", \"\"); // LOG2(x)\t返回以 2 为底的对数\n\t\tSQL_FUNCTION_MAP.put(\"max\", \"\"); // MAX(expression)\t返回字段 expression 中的最大值\n\t\tSQL_FUNCTION_MAP.put(\"min\", \"\"); // MIN(expression)\t返回字段 expression 中的最小值\n\t\tSQL_FUNCTION_MAP.put(\"mod\", \"\"); // MOD(x,y)\t返回 x 除以 y 以后的余数　\n\t\tSQL_FUNCTION_MAP.put(\"pi\", \"\"); // PI()\t返回圆周率(3.141593）　　\n\t\tSQL_FUNCTION_MAP.put(\"pow\", \"\"); // POW(x,y)\t返回 x 的 y 次方　\n\t\tSQL_FUNCTION_MAP.put(\"power\", \"\"); // POWER(x,y)\t返回 x 的 y 次方　\n\t\tSQL_FUNCTION_MAP.put(\"radians\", \"\"); // RADIANS(x)\t将角度转换为弧度　　\n\t\tSQL_FUNCTION_MAP.put(\"rand\", \"\"); // RAND()\t返回 0 到 1 的随机数　　\n\t\tSQL_FUNCTION_MAP.put(\"round\", \"\"); // ROUND(x)\t返回离 x 最近的整数\n\t\tSQL_FUNCTION_MAP.put(\"sign\", \"\"); // SIGN(x)\t返回 x 的符号，x 是负数、0、正数分别返回 -1、0 和 1　\n\t\tSQL_FUNCTION_MAP.put(\"sin\", \"\"); // SIN(x)\t求正弦值(参数是弧度)　　\n\t\tSQL_FUNCTION_MAP.put(\"sqrt\", \"\"); // SQRT(x)\t返回x的平方根　　\n\t\tSQL_FUNCTION_MAP.put(\"sum\", \"\"); // SUM(expression)\t返回指定字段的总和\n\t\tSQL_FUNCTION_MAP.put(\"tan\", \"\"); // TAN(x)\t求正切值(参数是弧度)\n\t\tSQL_FUNCTION_MAP.put(\"truncate\", \"\"); // TRUNCATE(x,y)\t返回数值 x 保留到小数点后 y 位的值（与 ROUND 最大的区别是不会进行四舍五入）\n\n\t\t// MySQL 时间与日期函数\n\t\tSQL_FUNCTION_MAP.put(\"adddate\", \"\"); // ADDDATE(d,n)\t计算起始日期 d 加上 n 天的日期\n\t\tSQL_FUNCTION_MAP.put(\"addtime\", \"\"); // ADDTIME(t,n)\tn 是一个时间表达式，时间 t 加上时间表达式 n\n\t\tSQL_FUNCTION_MAP.put(\"curdate\", \"\"); // CURDATE()\t返回当前日期\n\t\tSQL_FUNCTION_MAP.put(\"current_date\", \"\"); // CURRENT_DATE()\t返回当前日期\n\t\tSQL_FUNCTION_MAP.put(\"current_time\", \"\"); // CURRENT_TIME\t返回当前时间\n\t\tSQL_FUNCTION_MAP.put(\"current_timestamp\", \"\"); // CURRENT_TIMESTAMP()\t返回当前日期和时间\n\t\tSQL_FUNCTION_MAP.put(\"curtime\", \"\"); // CURTIME()\t返回当前时间\n\t\tSQL_FUNCTION_MAP.put(\"date\", \"\"); // DATE()\t从日期或日期时间表达式中提取日期值\n\t\tSQL_FUNCTION_MAP.put(\"datediff\", \"\"); // DATEDIFF(d1,d2)\t计算日期 d1->d2 之间相隔的天数\n\t\tSQL_FUNCTION_MAP.put(\"date_add\", \"\"); // DATE_ADD(d，INTERVAL expr type)\t计算起始日期 d 加上一个时间段后的日期\n\t\tSQL_FUNCTION_MAP.put(\"date_format\", \"\"); // DATE_FORMAT(d,f)\t按表达式 f的要求显示日期 d\n\t\tSQL_FUNCTION_MAP.put(\"date_sub\", \"\"); // DATE_SUB(date,INTERVAL expr type)\t函数从日期减去指定的时间间隔。\n\t\tSQL_FUNCTION_MAP.put(\"day\", \"\"); // DAY(d)\t返回日期值 d 的日期部分\n\t\tSQL_FUNCTION_MAP.put(\"dayname\", \"\"); // DAYNAME(d)\t返回日期 d 是星期几，如 Monday,Tuesday\n\t\tSQL_FUNCTION_MAP.put(\"dayofmonth\", \"\"); // DAYOFMONTH(d)\t计算日期 d 是本月的第几天\n\t\tSQL_FUNCTION_MAP.put(\"dayofweek\", \"\"); // DAYOFWEEK(d)\t日期 d 今天是星期几，1 星期日，2 星期一，以此类推\n\t\tSQL_FUNCTION_MAP.put(\"dayofyear\", \"\"); // DAYOFYEAR(d)\t计算日期 d 是本年的第几天\n\t\tSQL_FUNCTION_MAP.put(\"extract\", \"\"); // EXTRACT(type FROM d)\t从日期 d 中获取指定的值，type 指定返回的值。\n\t\tSQL_FUNCTION_MAP.put(\"from_days\", \"\"); // FROM_DAYS(n)\t计算从 0000 年 1 月 1 日开始 n 天后的日期\n\t\tSQL_FUNCTION_MAP.put(\"hour\", \"\"); // 'HOUR(t)\t返回 t 中的小时值\n\t\tSQL_FUNCTION_MAP.put(\"last_day\", \"\"); // LAST_DAY(d)\t返回给给定日期的那一月份的最后一天\n\t\tSQL_FUNCTION_MAP.put(\"localtime\", \"\"); // LOCALTIME()\t返回当前日期和时间\n\t\tSQL_FUNCTION_MAP.put(\"localtimestamp\", \"\"); // LOCALTIMESTAMP()\t返回当前日期和时间\n\t\tSQL_FUNCTION_MAP.put(\"makedate\", \"\"); // MAKEDATE(year, day-of-year)\t基于给定参数年份 year 和所在年中的天数序号 day-of-year 返回一个日期\n\t\tSQL_FUNCTION_MAP.put(\"maketime\", \"\"); // MAKETIME(hour, minute, second)\t组合时间，参数分别为小时、分钟、秒\n\t\tSQL_FUNCTION_MAP.put(\"microsecond\", \"\"); // MICROSECOND(date)\t返回日期参数所对应的微秒数\n\t\tSQL_FUNCTION_MAP.put(\"minute\", \"\"); // MINUTE(t)\t返回 t 中的分钟值\n\t\tSQL_FUNCTION_MAP.put(\"monthname\", \"\"); // MONTHNAME(d)\t返回日期当中的月份名称，如 November\n\t\tSQL_FUNCTION_MAP.put(\"month\", \"\"); // MONTH(d)\t返回日期d中的月份值，1 到 12\n\t\tSQL_FUNCTION_MAP.put(\"now\", \"\"); // NOW()\t返回当前日期和时间\n\t\tSQL_FUNCTION_MAP.put(\"period_add\", \"\"); // PERIOD_ADD(period, number)\t为 年-月 组合日期添加一个时段\n\t\tSQL_FUNCTION_MAP.put(\"period_diff\", \"\"); // PERIOD_DIFF(period1, period2)\t返回两个时段之间的月份差值\n\t\tSQL_FUNCTION_MAP.put(\"quarter\", \"\"); // QUARTER(d)\t返回日期d是第几季节，返回 1 到 4\n\t\tSQL_FUNCTION_MAP.put(\"second\", \"\"); // SECOND(t)\t返回 t 中的秒钟值\n\t\tSQL_FUNCTION_MAP.put(\"sec_to_time\", \"\"); // SEC_TO_TIME(sec, format); // )\t将以秒为单位的时间 s 转换为时分秒的格式\n\t\tSQL_FUNCTION_MAP.put(\"str_to_date\", \"\"); // STR_TO_DATE(string, format)\t将字符串转变为日期\n\t\tSQL_FUNCTION_MAP.put(\"subdate\", \"\"); // SUBDATE(d,n)\t日期 d 减去 n 天后的日期\n\t\tSQL_FUNCTION_MAP.put(\"subtime\", \"\"); // SUBTIME(t,n)\t时间 t 减去 n 秒的时间\n\t\tSQL_FUNCTION_MAP.put(\"sysdate\", \"\"); // SYSDATE()\t返回当前日期和时间\n\t\tSQL_FUNCTION_MAP.put(\"time\", \"\"); // TIME(expression)\t提取传入表达式的时间部分\n\t\tSQL_FUNCTION_MAP.put(\"time_format\", \"\"); // TIME_FORMAT(t,f)\t按表达式 f 的要求显示时间 t\n\t\tSQL_FUNCTION_MAP.put(\"time_to_sec\", \"\"); // TIME_TO_SEC(t)\t将时间 t 转换为秒\n\t\tSQL_FUNCTION_MAP.put(\"timediff\", \"\"); // TIMEDIFF(time1, time2)\t计算时间差值\n\t\tSQL_FUNCTION_MAP.put(\"timestamp\", \"\"); // TIMESTAMP(expression, interval)\t单个参数时，函数返回日期或日期时间表达式；有2个参数时，将参数加和\n\t\tSQL_FUNCTION_MAP.put(\"to_days\", \"\"); // TO_DAYS(d)\t计算日期 d 距离 0000 年 1 月 1 日的天数\n\t\tSQL_FUNCTION_MAP.put(\"week\", \"\"); // WEEK(d)\t计算日期 d 是本年的第几个星期，范围是 0 到 53\n\t\tSQL_FUNCTION_MAP.put(\"weekday\", \"\"); // WEEKDAY(d)\t日期 d 是星期几，0 表示星期一，1 表示星期二\n\t\tSQL_FUNCTION_MAP.put(\"weekofyear\", \"\"); // WEEKOFYEAR(d)\t计算日期 d 是本年的第几个星期，范围是 0 到 53\n\t\tSQL_FUNCTION_MAP.put(\"year\", \"\"); // YEAR(d)\t返回年份\n\t\tSQL_FUNCTION_MAP.put(\"yearweek\", \"\"); // YEARWEEK(date, mode)\t返回年份及第几周（0到53），mode 中 0 表示周天，1表示周一，以此类推\n\t\tSQL_FUNCTION_MAP.put(\"unix_timestamp\", \"\"); // UNIX_TIMESTAMP(date)\t获取UNIX时间戳函数，返回一个以 UNIX 时间戳为基础的无符号整数\n\t\tSQL_FUNCTION_MAP.put(\"from_unixtime\", \"\"); // FROM_UNIXTIME(date)\t将 UNIX 时间戳转换为时间格式，与UNIX_TIMESTAMP互为反函数\n\n\t\t// MYSQL JSON 函数\n\t\tSQL_FUNCTION_MAP.put(\"json_append\", \"\"); // JSON_APPEND(json_doc, path, val[, path, val] ...)) 插入JSON数组\n\t\tSQL_FUNCTION_MAP.put(\"json_array\", \"\"); // JSON_ARRAY(val1, val2...) 创建JSON数组\n\t\tSQL_FUNCTION_MAP.put(\"json_array_append\", \"\"); // JSON_ARRAY_APPEND(json_doc, val) 将数据附加到JSON文档\n\t\tSQL_FUNCTION_MAP.put(\"json_array_insert\", \"\"); // JSON_ARRAY_INSERT(json_doc, val) 插入JSON数组\n\t\tSQL_FUNCTION_MAP.put(\"json_array_get\", \"\"); // JSON_ARRAY_GET(json_doc, position) 从JSON数组提取指定位置的元素\n\t\tSQL_FUNCTION_MAP.put(\"json_contains\", \"\"); // JSON_CONTAINS(json_doc, val) JSON文档是否在路径中包含特定对象\n\t\tSQL_FUNCTION_MAP.put(\"json_array_contains\", \"\"); // JSON_ARRAY_CONTAINS(json_doc, path) JSON文档是否在路径中包含特定对象\n\t\tSQL_FUNCTION_MAP.put(\"json_contains_path\", \"\"); // JSON_CONTAINS_PATH(json_doc, path) JSON文档是否在路径中包含任何数据\n\t\tSQL_FUNCTION_MAP.put(\"json_depth\", \"\"); // JSON_DEPTH(json_doc) JSON文档的最大深度\n\t\tSQL_FUNCTION_MAP.put(\"json_extract\", \"\"); // JSON_EXTRACT(json_doc, path) 从JSON文档返回数据\n\t\tSQL_FUNCTION_MAP.put(\"json_extract_scalar\", \"\"); // JSON_EXTRACT_SCALAR(json_doc, path) 从JSON文档返回基础类型数据，例如 Boolean, Number, String\n\t\tSQL_FUNCTION_MAP.put(\"json_insert\", \"\"); // JSON_INSERT(json_doc, val) 将数据插入JSON文档\n\t\tSQL_FUNCTION_MAP.put(\"json_keys\", \"\"); // JSON_KEYS(json_doc[, path]) JSON文档中的键数组\n\t\tSQL_FUNCTION_MAP.put(\"json_length\", \"\"); // JSON_LENGTH(json_doc) JSON文档中的元素数\n\t\tSQL_FUNCTION_MAP.put(\"json_size\", \"\"); // JSON_SIZE(json_doc) JSON文档中的元素数\n\t\tSQL_FUNCTION_MAP.put(\"json_array_length\", \"\"); // JSON_ARRAY_LENGTH(json_doc) JSON文档中的元素数\n\t\tSQL_FUNCTION_MAP.put(\"json_format\", \"\"); // JSON_FORMAT(json_doc) 格式化 JSON\n\t\tSQL_FUNCTION_MAP.put(\"json_parse\", \"\"); // JSON_PARSE(val) 转换为 JSON\n\t\tSQL_FUNCTION_MAP.put(\"json_merge\", \"\"); // JSON_MERGE(json_doc1, json_doc2) （已弃用） 合并JSON文档，保留重复的键。JSON_MERGE_PRESERVE（）的已弃用同义词\n\t\tSQL_FUNCTION_MAP.put(\"json_merge_patch\", \"\"); // JSON_MERGE_PATCH(json_doc1, json_doc2) 合并JSON文档，替换重复键的值\n\t\tSQL_FUNCTION_MAP.put(\"json_merge_preserve\", \"\"); // JSON_MERGE_PRESERVE(json_doc1, json_doc2) 合并JSON文档，保留重复的键\n\t\tSQL_FUNCTION_MAP.put(\"json_object\", \"\"); // JSON_OBJECT(key1, val1, key2, val2...) 创建JSON对象\n\t\tSQL_FUNCTION_MAP.put(\"json_overlaps\", \"\"); // JSON_OVERLAPS(json_doc1, json_doc2) （引入8.0.17） 比较两个JSON文档，如果它们具有相同的键值对或数组元素，则返回TRUE（1），否则返回FALSE（0）\n\t\tSQL_FUNCTION_MAP.put(\"json_pretty\", \"\"); // JSON_PRETTY(json_doc) 以易于阅读的格式打印JSON文档\n\t\tSQL_FUNCTION_MAP.put(\"json_quote\", \"\"); // JSON_QUOTE(json_doc1) 引用JSON文档\n\t\tSQL_FUNCTION_MAP.put(\"json_remove\", \"\"); // JSON_REMOVE(json_doc1, path) 从JSON文档中删除数据\n\t\tSQL_FUNCTION_MAP.put(\"json_replace\", \"\"); // JSON_REPLACE(json_doc1, val1, val2) 替换JSON文档中的值\n\t\tSQL_FUNCTION_MAP.put(\"json_schema_valid\", \"\"); // JSON_SCHEMA_VALID(json_doc) （引入8.0.17） 根据JSON模式验证JSON文档；如果文档针对架构进行验证，则返回TRUE / 1；否则，则返回FALSE / 0\n\t\tSQL_FUNCTION_MAP.put(\"json_schema_validation_report\", \"\"); // JSON_SCHEMA_VALIDATION_REPORT(json_doc, mode) （引入8.0.17） 根据JSON模式验证JSON文档；以JSON格式返回有关验证结果的报告，包括成功或失败以及失败原因\n\t\tSQL_FUNCTION_MAP.put(\"json_search\", \"\"); // JSON_SEARCH(json_doc, val) JSON文档中值的路径\n\t\tSQL_FUNCTION_MAP.put(\"json_set\", \"\"); // JSON_SET(json_doc, val) 将数据插入JSON文档\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"json_storage_free\", \"\"); // JSON_STORAGE_FREE() 部分更新后，JSON列值的二进制表示形式中的可用空间\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"json_storage_size\", \"\"); // JSON_STORAGE_SIZE() 用于存储JSON文档的二进制表示的空间\n\t\tSQL_FUNCTION_MAP.put(\"json_table\", \"\"); // JSON_TABLE() 从JSON表达式返回数据作为关系表\n\t\tSQL_FUNCTION_MAP.put(\"json_type\", \"\"); // JSON_TYPE(json_doc) JSON值类型\n\t\tSQL_FUNCTION_MAP.put(\"json_unquote\", \"\"); // JSON_UNQUOTE(json_doc) 取消引用JSON值\n\t\tSQL_FUNCTION_MAP.put(\"json_valid\", \"\"); // JSON_VALID(json_doc) JSON值是否有效\n\t\tSQL_FUNCTION_MAP.put(\"json_arrayagg\", \"\"); // JSON_ARRAYAGG(key) 将每个表达式转换为 JSON 值，然后返回一个包含这些 JSON 值的 JSON 数组\n\t\tSQL_FUNCTION_MAP.put(\"json_objectagg\", \"\"); // JSON_OBJECTAGG(key, val))  将每个表达式转换为 JSON 值，然后返回一个包含这些 JSON 值的 JSON 对象\n\t\tSQL_FUNCTION_MAP.put(\"is_json_scalar\", \"\"); // IS_JSON_SCALAR(val))  是否为JSON基本类型，例如 Boolean, Number, String\n\n\t\t// MySQL 高级函数\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"bin\", \"\"); // BIN(x)\t返回 x 的二进制编码\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"binary\", \"\"); // BINARY(s)\t将字符串 s 转换为二进制字符串\n\t\tSQL_FUNCTION_MAP.put(\"case\", \"\"); // CASE 表示函数开始，END 表示函数结束。如果 condition1 成立，则返回 result1, 如果 condition2 成立，则返回 result2，当全部不成立则返回 result，而当有一个成立之后，后面的就不执行了。\n\t\tSQL_FUNCTION_MAP.put(\"cast\", \"\"); // CAST(x AS type)\t转换数据类型\n\t\tSQL_FUNCTION_MAP.put(\"coalesce\", \"\"); // COALESCE(expr1, expr2, ...., expr_n)\t返回参数中的第一个非空表达式（从左向右）\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"conv\", \"\"); // CONV(x,f1,f2)\t返回 f1 进制数变成 f2 进制数\n\t\t//\t\tSQL_FUNCTION_MAP.put(\"convert\", \"\"); // CONVERT(s, cs)\t函数将字符串 s 的字符集变成 cs\n\t\tSQL_FUNCTION_MAP.put(\"if\", \"\"); // IF(expr,v1,v2)\t如果表达式 expr 成立，返回结果 v1；否则，返回结果 v2。\n\t\tSQL_FUNCTION_MAP.put(\"ifnull\", \"\"); // IFNULL(v1,v2)  如果 v1 的值不为 NULL，则返回 v1，否则返回 v2。\n\t\tSQL_FUNCTION_MAP.put(\"isnull\", \"\"); // ISNULL(expression)\t判断表达式是否为 NULL\n\t\tSQL_FUNCTION_MAP.put(\"nullif\", \"\"); // NULLIF(expr1, expr2)  比较两个字符串，如果字符串 expr1 与 expr2 相等 返回 NULL，否则返回 expr1\n\t\tSQL_FUNCTION_MAP.put(\"group_concat\", \"\"); // GROUP_CONCAT([DISTINCT], s1, s2...)  聚合拼接字符串\n\t\tSQL_FUNCTION_MAP.put(\"match\", \"\"); // MATCH (name,tag) AGAINST ('a b' IN NATURAL LANGUAGE MODE)  全文检索\n\t\tSQL_FUNCTION_MAP.put(\"any_value\", \"\"); // any_value(userId) 解决 ONLY_FULL_GROUP_BY 报错\n\n\n\t\t// ClickHouse 字符串函数  注释的函数表示返回的格式暂时不支持，如：返回数组 ，同时包含因版本不同 clickhosue不支持的函数，版本\n\t\tSQL_FUNCTION_MAP.put(\"empty\", \"\"); // empty(s) 对于空字符串s返回1，对于非空字符串返回0\n\t\tSQL_FUNCTION_MAP.put(\"notEmpty\", \"\"); // notEmpty(s) 对于空字符串返回0，对于非空字符串返回1。\n\t\tSQL_FUNCTION_MAP.put(\"lengthUTF8\", \"\"); // 假定字符串以UTF-8编码组成的文本，返回此字符串的Unicode字符长度。如果传入的字符串不是UTF-8编码，则函数可能返回一个预期外的值\n\t\tSQL_FUNCTION_MAP.put(\"lcase\", \"\"); // 将字符串中的ASCII转换为小写\n\t\tSQL_FUNCTION_MAP.put(\"ucase\", \"\"); // 将字符串中的ASCII转换为大写。\n\t\tSQL_FUNCTION_MAP.put(\"lowerUTF8\", \"\"); // 将字符串转换为小写，函数假设字符串是以UTF-8编码文本的字符集。\n\t\tSQL_FUNCTION_MAP.put(\"upperUTF8\", \"\"); // 将字符串转换为大写，函数假设字符串是以UTF-8编码文本的字符集。\n\t\tSQL_FUNCTION_MAP.put(\"isValidUTF8\", \"\"); // 检查字符串是否为有效的UTF-8编码，是则返回1，否则返回0。\n\t\tSQL_FUNCTION_MAP.put(\"toValidUTF8\", \"\"); // 用（U+FFFD）字符替换无效的UTF-8字符。所有连续的无效字符都会被替换为一个替换字符。\n\t\tSQL_FUNCTION_MAP.put(\"reverseUTF8\", \"\"); // 以Unicode字符为单位反转UTF-8编码的字符串。\n\t\tSQL_FUNCTION_MAP.put(\"concatAssumeInjective\", \"\"); // concatAssumeInjective(s1, s2, …) 与concat相同，区别在于，你需要保证concat(s1, s2, s3) -> s4是单射的，它将用于GROUP BY的优化。\n\t\tSQL_FUNCTION_MAP.put(\"substringUTF8\", \"\"); // substringUTF8(s,offset,length)¶ 与’substring’相同，但其操作单位为Unicode字符，函数假设字符串是以UTF-8进行编码的文本。如果不是则可能返回一个预期外的结果（不会抛出异常）。\n\t\tSQL_FUNCTION_MAP.put(\"appendTrailingCharIfAbsent\", \"\"); // appendTrailingCharIfAbsent(s,c) 如果’s’字符串非空并且末尾不包含’c’字符，则将’c’字符附加到末尾\n\t\tSQL_FUNCTION_MAP.put(\"convertCharset\", \"\"); // convertCharset(s,from,to) 返回从’from’中的编码转换为’to’中的编码的字符串’s’。\n\t\tSQL_FUNCTION_MAP.put(\"base64Encode\", \"\"); // base64Encode(s) 将字符串’s’编码成base64\n\t\tSQL_FUNCTION_MAP.put(\"base64Decode\", \"\"); // base64Decode(s)  使用base64将字符串解码成原始字符串。如果失败则抛出异常。\n\t\tSQL_FUNCTION_MAP.put(\"tryBase64Decode\", \"\"); // tryBase64Decode(s) 使用base64将字符串解码成原始字符串。但如果出现错误，将返回空字符串。\n\t\tSQL_FUNCTION_MAP.put(\"endsWith\", \"\"); // endsWith(s,后缀) 返回是否以指定的后缀结尾。如果字符串以指定的后缀结束，则返回1，否则返回0。\n\t\tSQL_FUNCTION_MAP.put(\"startsWith\", \"\"); // startsWith（s，前缀) 返回是否以指定的前缀开头。如果字符串以指定的前缀开头，则返回1，否则返回0。\n\t\tSQL_FUNCTION_MAP.put(\"trimLeft\", \"\"); // trimLeft(s)返回一个字符串，用于删除左侧的空白字符。\n\t\tSQL_FUNCTION_MAP.put(\"trimRight\", \"\"); // trimRight(s) 返回一个字符串，用于删除右侧的空白字符。\n\t\tSQL_FUNCTION_MAP.put(\"trimBoth\", \"\"); // trimBoth(s)，用于删除任一侧的空白字符\n\t\tSQL_FUNCTION_MAP.put(\"extractAllGroups\", \"\"); // extractAllGroups(text, regexp) 从正则表达式匹配的非重叠子字符串中提取所有组\n\t\t// SQL_FUNCTION_MAP.put(\"leftPad\", \"\"); // leftPad('string', 'length'[, 'pad_string']) 用空格或指定的字符串从左边填充当前字符串(如果需要，可以多次)，直到得到的字符串达到给定的长度\n\t\t// SQL_FUNCTION_MAP.put(\"leftPadUTF8\", \"\"); // leftPadUTF8('string','length'[, 'pad_string']) 用空格或指定的字符串从左边填充当前字符串(如果需要，可以多次)，直到得到的字符串达到给定的长度\n\t\t// SQL_FUNCTION_MAP.put(\"rightPad\", \"\"); // rightPad('string', 'length'[, 'pad_string']) 用空格或指定的字符串(如果需要，可以多次)从右边填充当前字符串，直到得到的字符串达到给定的长度\n\t\t// SQL_FUNCTION_MAP.put(\"rightPadUTF8\", \"\");// rightPadUTF8('string','length'[, 'pad_string'])  用空格或指定的字符串(如果需要，可以多次)从右边填充当前字符串，直到得到的字符串达到给定的长度。\n\t\tSQL_FUNCTION_MAP.put(\"normalizeQuery\", \"\"); // normalizeQuery(x)  用占位符替换文字、文字序列和复杂的别名。\n\t\tSQL_FUNCTION_MAP.put(\"normalizedQueryHash\", \"\"); // normalizedQueryHash(x) 为类似查询返回相同的64位散列值，但不包含文字值。有助于对查询日志进行分析\n\t\tSQL_FUNCTION_MAP.put(\"positionUTF8\", \"\"); // positionUTF8(s, needle[, start_pos]) 返回在字符串中找到的子字符串的位置(以Unicode点表示)，从1开始。\n\t\tSQL_FUNCTION_MAP.put(\"multiSearchFirstIndex\", \"\"); // multiSearchFirstIndex(s, [needle1, needle2, …, needlen]) 返回字符串s中最左边的needlei的索引i(从1开始)，否则返回0\n\t\tSQL_FUNCTION_MAP.put(\"multiSearchAny\", \"\"); // multiSearchAny(s, [needle1, needle2, …, needlen])如果至少有一个字符串needlei匹配字符串s，则返回1，否则返回0。\n\t\tSQL_FUNCTION_MAP.put(\"match\", \"\"); // match(s, pattern) 检查字符串是否与模式正则表达式匹配。re2正则表达式。re2正则表达式的语法比Perl正则表达式的语法更有局限性。\n\t\tSQL_FUNCTION_MAP.put(\"multiMatchAny\", \"\"); // multiMatchAny(s, [pattern1, pattern2, …, patternn]) 与match相同，但是如果没有匹配的正则表达式返回0，如果有匹配的模式返回1\n\t\tSQL_FUNCTION_MAP.put(\"multiMatchAnyIndex\", \"\"); // multiMatchAnyIndex(s, [pattern1, pattern2, …, patternn]) 与multiMatchAny相同，但返回与干堆匹配的任何索引\n\t\tSQL_FUNCTION_MAP.put(\"extract\", \"\"); // extract(s, pattern)  使用正则表达式提取字符串的片段\n\t\tSQL_FUNCTION_MAP.put(\"extractAll\", \"\"); // extractAll(s, pattern) 使用正则表达式提取字符串的所有片段\n\t\tSQL_FUNCTION_MAP.put(\"like\", \"\"); // like(s, pattern) 检查字符串是否与简单正则表达式匹配\n\t\tSQL_FUNCTION_MAP.put(\"notLike\", \"\"); // 和‘like’是一样的，但是是否定的\n\t\tSQL_FUNCTION_MAP.put(\"countSubstrings\", \"\"); // countSubstrings(s, needle[, start_pos])返回子字符串出现的次数\n\t\tSQL_FUNCTION_MAP.put(\"countMatches\", \"\"); // 返回干s中的正则表达式匹配数。countMatches(s, pattern)\n\t\tSQL_FUNCTION_MAP.put(\"replaceOne\", \"\"); // replaceOne(s, pattern, replacement)将' s '中的' pattern '子串的第一个出现替换为' replacement '子串。\n\n\t\tSQL_FUNCTION_MAP.put(\"replaceAll\", \"\"); // replaceAll(s, pattern, replacement)/用' replacement '子串替换' s '中所有出现的' pattern '子串\n\t\tSQL_FUNCTION_MAP.put(\"replaceRegexpOne\", \"\"); // replaceRegexpOne(s, pattern, replacement)使用' pattern '正则表达式进行替换\n\t\tSQL_FUNCTION_MAP.put(\"replaceRegexpAll\", \"\"); // replaceRegexpAll(s, pattern, replacement)\n\t\tSQL_FUNCTION_MAP.put(\"regexpQuoteMeta\", \"\"); // regexpQuoteMeta(s)该函数在字符串中某些预定义字符之前添加一个反斜杠\n\n\t\t// clickhouse日期函数\n\t\tSQL_FUNCTION_MAP.put(\"toYear\", \"\"); // 将Date或DateTime转换为包含年份编号（AD）的UInt16类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toQuarter\", \"\"); // 将Date或DateTime转换为包含季度编号的UInt8类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toMonth\", \"\"); // Date或DateTime转换为包含月份编号（1-12）的UInt8类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toDayOfYear\", \"\"); // 将Date或DateTime转换为包含一年中的某一天的编号的UInt16（1-366）类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toDayOfMonth\", \"\");// 将Date或DateTime转换为包含一月中的某一天的编号的UInt8（1-31）类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toDayOfWeek\", \"\"); // 将Date或DateTime转换为包含一周中的某一天的编号的UInt8（周一是1, 周日是7）类型的数字。\n\t\tSQL_FUNCTION_MAP.put(\"toHour\", \"\"); // 将DateTime转换为包含24小时制（0-23）小时数的UInt8数字。\n\t\tSQL_FUNCTION_MAP.put(\"toMinute\", \"\"); // 将DateTime转换为包含一小时中分钟数（0-59）的UInt8数字。\n\t\tSQL_FUNCTION_MAP.put(\"toSecond\", \"\"); // 将DateTime转换为包含一分钟中秒数（0-59）的UInt8数字。\n\t\tSQL_FUNCTION_MAP.put(\"toUnixTimestamp\", \"\"); // 对于DateTime参数：将值转换为UInt32类型的数字-Unix时间戳\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfYear\", \"\"); // 将Date或DateTime向前取整到本年的第一天。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfISOYear\", \"\"); // 将Date或DateTime向前取整到ISO本年的第一天。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfQuarter\", \"\");// 将Date或DateTime向前取整到本季度的第一天。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfMonth\", \"\"); // 将Date或DateTime向前取整到本月的第一天。\n\t\tSQL_FUNCTION_MAP.put(\"toMonday\", \"\");  // 将Date或DateTime向前取整到本周的星期\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfWeek\", \"\"); // 按mode将Date或DateTime向前取整到最近的星期日或星期一。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfDay\", \"\"); // 将DateTime向前取整到今天的开始。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfHour\", \"\"); // 将DateTime向前取整到当前小时的开始。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfMinute\", \"\"); // 将DateTime向前取整到当前分钟的开始。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfSecond\", \"\"); // 将DateTime向前取整到当前秒数的开始。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfFiveMinute\", \"\");// 将DateTime以五分钟为单位向前取整到最接近的时间点。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfTenMinutes\", \"\"); // 将DateTime以十分钟为单位向前取整到最接近的时间点。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfFifteenMinutes\", \"\"); // 将DateTime以十五分钟为单位向前取整到最接近的时间点。\n\t\tSQL_FUNCTION_MAP.put(\"toStartOfInterval\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"toTime\", \"\"); // 将DateTime中的日期转换为一个固定的日期，同时保留时间部分。\n\t\tSQL_FUNCTION_MAP.put(\"toISOYear\", \"\"); // 将Date或DateTime转换为包含ISO年份的UInt16类型的编号。\n\t\tSQL_FUNCTION_MAP.put(\"toISOWeek\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"toWeek\", \"\");// 返回Date或DateTime的周数。\n\t\tSQL_FUNCTION_MAP.put(\"toYearWeek\", \"\"); // 返回年和周的日期\n\t\tSQL_FUNCTION_MAP.put(\"date_trunc\", \"\"); // 截断日期和时间数据到日期的指定部分\n\t\tSQL_FUNCTION_MAP.put(\"date_diff\", \"\"); // 回两个日期或带有时间值的日期之间的差值。\n\n\t\tSQL_FUNCTION_MAP.put(\"yesterday\", \"\"); // 不接受任何参数并在请求执行时的某一刻返回昨天的日期(Date)。\n\t\tSQL_FUNCTION_MAP.put(\"today\", \"\"); // 不接受任何参数并在请求执行时的某一刻返回当前日期(Date)。\n\t\tSQL_FUNCTION_MAP.put(\"timeSlot\", \"\"); // 将时间向前取整半小时。\n\t\tSQL_FUNCTION_MAP.put(\"toYYYYMM\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"toYYYYMMDD\", \"\");//\n\t\tSQL_FUNCTION_MAP.put(\"toYYYYMMDDhhmmss\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"addYears\", \"\"); // Function adds a Date/DateTime interval to a Date/DateTime and then return the Date/DateTime\n\t\tSQL_FUNCTION_MAP.put(\"addMonths\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"addWeeks\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"addDays\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"addHours\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"addMinutes\", \"\");// 同上\n\t\tSQL_FUNCTION_MAP.put(\"addSeconds\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"addQuarters\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractYears\", \"\"); // Function subtract a Date/DateTime interval to a Date/DateTime and then return the Date/DateTime\n\t\tSQL_FUNCTION_MAP.put(\"subtractMonths\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractWeeks\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractDays\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractours\", \"\");// 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractMinutes\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractSeconds\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"subtractQuarters\", \"\"); // 同上\n\t\tSQL_FUNCTION_MAP.put(\"formatDateTime\", \"\"); // 函数根据给定的格式字符串来格式化时间\n\t\tSQL_FUNCTION_MAP.put(\"timestamp_add\", \"\"); // 使用提供的日期或日期时间值添加指定的时间值。\n\t\tSQL_FUNCTION_MAP.put(\"timestamp_sub\", \"\"); // 从提供的日期或带时间的日期中减去时间间隔。\n\n\t\t// ClickHouse json函数\n\t\tSQL_FUNCTION_MAP.put(\"visitParamHas\", \"\"); // visitParamHas(params, name)检查是否存在«name»名称的字段\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractUInt\", \"\"); // visitParamExtractUInt(params, name)将名为«name»的字段的值解析成UInt64。\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractInt\", \"\"); // 与visitParamExtractUInt相同，但返回Int64。\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractFloat\", \"\"); // 与visitParamExtractUInt相同，但返回Float64。\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractBool\", \"\");// 解析true/false值。其结果是UInt8类型的。\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractRaw\", \"\"); // 返回字段的值，包含空格符。\n\t\tSQL_FUNCTION_MAP.put(\"visitParamExtractString\", \"\"); // 使用双引号解析字符串。这个值没有进行转义。如果转义失败，它将返回一个空白字符串。\n\t\tSQL_FUNCTION_MAP.put(\"JSONHas\", \"\"); // 如果JSON中存在该值，则返回1。\n\t\tSQL_FUNCTION_MAP.put(\"JSONLength\", \"\"); // 返回JSON数组或JSON对象的长度。\n\t\tSQL_FUNCTION_MAP.put(\"JSONType\", \"\"); // 返回JSON值的类型。\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractUInt\", \"\"); // 解析JSON并提取值。这些函数类似于visitParam*函数。\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractInt\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractFloat\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractBool\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractString\", \"\"); // 解析JSON并提取字符串。此函数类似于visitParamExtractString函数。\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtract\", \"\"); // 解析JSON并提取给定ClickHouse数据类型的值。\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractKeysAndValues\", \"\"); // 从JSON中解析键值对，其中值是给定的ClickHouse数据类型\n\t\tSQL_FUNCTION_MAP.put(\"JSONExtractRaw\", \"\"); // 返回JSON的部分。\n\t\tSQL_FUNCTION_MAP.put(\"toJSONString\", \"\"); //\n\n\t\t// ClickHouse 类型转换函数\n\t\tSQL_FUNCTION_MAP.put(\"toInt8\", \"\"); // toInt8(expr)  转换一个输入值为Int类型\n\t\tSQL_FUNCTION_MAP.put(\"toInt16\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt32\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt64\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt8OrZero\", \"\"); // toInt(8|16|32|64)OrZero 尝试把字符串转为 Int\n\t\tSQL_FUNCTION_MAP.put(\"toInt16OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt32OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt64OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt8OrNull\", \"\");// toInt(8|16|32|64)O 尝试把字符串转为 Int\n\t\tSQL_FUNCTION_MAP.put(\"toInt16OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt32OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toInt64OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt8\", \"\"); // toInt8(expr)  转换一个输入值为Int类型\n\t\tSQL_FUNCTION_MAP.put(\"toUInt16\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt32\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt64\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt8OrZero\", \"\"); // toInt(8|16|32|64)OrZero 尝试把字符串转为 Int\n\t\tSQL_FUNCTION_MAP.put(\"toUInt16OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt32OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt64OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt8OrNull\", \"\"); // toInt(8|16|32|64)OrNull 尝试把字符串转为 Int\n\t\tSQL_FUNCTION_MAP.put(\"toUInt16OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt32OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toUInt64OrNull\", \"\");\n\n\t\tSQL_FUNCTION_MAP.put(\"toFloat32\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFloat64\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFloat32OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFloat64OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFloat32OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFloat64OrNull\", \"\");\n\n\t\tSQL_FUNCTION_MAP.put(\"toDate\", \"\"); //\n\t\tSQL_FUNCTION_MAP.put(\"toDateOrZero\", \"\"); // toInt16(expr)\n\t\tSQL_FUNCTION_MAP.put(\"toDateOrNull\", \"\"); // toInt32(expr)\n\t\tSQL_FUNCTION_MAP.put(\"toDateTimeOrZero\", \"\"); // toInt64(expr) 尝试把字符串转为 DateTime\n\t\tSQL_FUNCTION_MAP.put(\"toDateTimeOrNull\", \"\"); // toInt(8|16|32|64) 尝试把字符串转为 DateTime\n\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal32\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toFixedString\", \"\"); // 将String类型的参数转换为FixedString(N)类型的值\n\t\tSQL_FUNCTION_MAP.put(\"toStringCutToZero\", \"\"); // 接受String或FixedString参数,返回String，其内容在找到的第一个零字节处被截断。\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal256\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal32OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal64OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal128OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal256OrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal32OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal64OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal128OrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toDecimal256OrZero\", \"\");\n\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalSecond\", \"\"); // 把一个数值类型的值转换为Interval类型的数据。\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalMinute\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalHour\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalDay\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalWeek\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalMonth\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalQuarter\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toIntervalYear\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"parseDateTimeBestEffort\", \"\"); // 把String类型的时间日期转换为DateTime数据类型。\n\t\tSQL_FUNCTION_MAP.put(\"parseDateTimeBestEffortOrNull\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"parseDateTimeBestEffortOrZero\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"toLowCardinality\", \"\");\n\n\n\t\t// ClickHouse hash 函数\n\t\tSQL_FUNCTION_MAP.put(\"halfMD5\", \"\"); // 计算字符串的MD5。然后获取结果的前8个字节并将它们作为UInt64（大端）返回\n\t\tSQL_FUNCTION_MAP.put(\"MD5\", \"\"); // 计算字符串的MD5并将结果放入FixedString(16)中返回\n\n\t\t// ClickHouse ip 地址函数\n\t\tSQL_FUNCTION_MAP.put(\"IPv4NumToString\", \"\"); // 接受一个 UInt32（大端）表示的IPv4的地址，返回相应IPv4的字符串表现形式\n\t\tSQL_FUNCTION_MAP.put(\"IPv4StringToNum\", \"\"); // 与IPv4NumToString函数相反。如果IPv4地址格式无效，则返回0。\n\t\tSQL_FUNCTION_MAP.put(\"IPv6NumToString\", \"\"); // 接受FixedString(16)类型的二进制格式的IPv6地址。以文本格式返回此地址的字符串。\n\t\tSQL_FUNCTION_MAP.put(\"IPv6StringToNum\", \"\"); // 与IPv6NumToString的相反。如果IPv6地址格式无效，则返回空字节字符串。\n\t\tSQL_FUNCTION_MAP.put(\"IPv4ToIPv6\", \"\"); // 接受一个UInt32类型的IPv4地址，返回FixedString(16)类型的IPv6地址\n\t\tSQL_FUNCTION_MAP.put(\"cutIPv6\", \"\"); // 接受一个FixedString(16)类型的IPv6地址，返回 String，包含删除指定位之后的地址的文本格\n\t\tSQL_FUNCTION_MAP.put(\"toIPv4\", \"\"); // IPv4StringToNum()的别名，\n\t\tSQL_FUNCTION_MAP.put(\"toIPv6\", \"\"); // IPv6StringToNum()的别名\n\t\tSQL_FUNCTION_MAP.put(\"isIPAddressInRange\", \"\"); // 确定一个IP地址是否包含在以CIDR符号表示的网络中\n\n\t\t// ClickHouse Nullable 处理函数\n\t\tSQL_FUNCTION_MAP.put(\"isNull\", \"\"); // 检查参数是否为NULL。\n\t\tSQL_FUNCTION_MAP.put(\"isNotNull\", \"\"); // 检查参数是否不为 NULL.\n\t\tSQL_FUNCTION_MAP.put(\"ifNull\", \"\"); // 如果第一个参数为«NULL»，则返回第二个参数的值。\n\t\tSQL_FUNCTION_MAP.put(\"assumeNotNull\", \"\"); // 将可为空类型的值转换为非Nullable类型的值。\n\t\tSQL_FUNCTION_MAP.put(\"toNullable\", \"\"); // 将参数的类型转换为Nullable。\n\n\t\t// ClickHouse UUID 函数\n\t\tSQL_FUNCTION_MAP.put(\"generateUUIDv4\", \"\"); // 生成一个UUID\n\t\tSQL_FUNCTION_MAP.put(\"toUUID\", \"\"); // toUUID(x) 将String类型的值转换为UUID类型的值。\n\n\t\t// ClickHouse 系统函数\n\t\tSQL_FUNCTION_MAP.put(\"hostName\", \"\"); // hostName()回一个字符串，其中包含执行此函数的主机的名称。\n\t\tSQL_FUNCTION_MAP.put(\"getMacro\", \"\"); // 从服务器配置的宏部分获取指定值。\n\t\tSQL_FUNCTION_MAP.put(\"FQDN\", \"\"); // 返回完全限定的域名。\n\t\tSQL_FUNCTION_MAP.put(\"basename\", \"\"); // 提取字符串最后一个斜杠或反斜杠之后的尾随部分\n\t\tSQL_FUNCTION_MAP.put(\"currentUser\", \"\"); // 返回当前用户的登录。在分布式查询的情况下，将返回用户的登录，即发起的查询\n\t\tSQL_FUNCTION_MAP.put(\"version\", \"\"); // 以字符串形式返回服务器版本。\n\t\tSQL_FUNCTION_MAP.put(\"uptime\", \"\"); // 以秒为单位返回服务器的正常运行时间。\n\n\t\t// ClickHouse 数学函数\n\t\tSQL_FUNCTION_MAP.put(\"least\", \"\"); // least(a, b) 返回a和b中最小的值。\n\t\tSQL_FUNCTION_MAP.put(\"greatest\", \"\"); // greatest(a, b) 返回a和b的最大值。\n\t\tSQL_FUNCTION_MAP.put(\"plus\", \"\"); // plus(a, b), a + b operator¶计算数值的总和。\n\t\tSQL_FUNCTION_MAP.put(\"minus\", \"\"); // minus(a, b), a - b operator 计算数值之间的差，结果总是有符号的。\n\t\tSQL_FUNCTION_MAP.put(\"multiply\", \"\");// multiply(a, b), a * b operator 计算数值的乘积\n\t\tSQL_FUNCTION_MAP.put(\"divide\", \"\"); // divide(a, b), a / b operator 计算数值的商。结果类型始终是浮点类型\n\t\tSQL_FUNCTION_MAP.put(\"intDiv\", \"\"); // intDiv(a,b)计算数值的商，向下舍入取整（按绝对值）。\n\t\tSQL_FUNCTION_MAP.put(\"intDivOrZero\", \"\"); // intDivOrZero(a,b)与’intDiv’的不同之处在于它在除以零或将最小负数除以-1时返回零。\n\t\tSQL_FUNCTION_MAP.put(\"modulo\", \"\"); // modulo(a, b), a % b operator 计算除法后的余数。\n\t\tSQL_FUNCTION_MAP.put(\"moduloOrZero\", \"\"); // 和modulo不同之处在于，除以0时结果返回0\n\t\tSQL_FUNCTION_MAP.put(\"negate\", \"\"); // 通过改变数值的符号位对数值取反，结果总是有符号\n\t\tSQL_FUNCTION_MAP.put(\"gcd\", \"\"); // gcd(a,b) 返回数值的最大公约数。\n\t\tSQL_FUNCTION_MAP.put(\"lcm\", \"\"); // lcm(a,b) 返回数值的最小公倍数\n\t\tSQL_FUNCTION_MAP.put(\"e\", \"\"); // e() 返回一个接近数学常量e的Float64数字。\n\t\tSQL_FUNCTION_MAP.put(\"pi\", \"\"); // pi() 返回一个接近数学常量π的Float64数字。\n\t\tSQL_FUNCTION_MAP.put(\"exp2\", \"\"); // exp2(x)¶接受一个数值类型的参数并返回它的2的x次幂。\n\t\tSQL_FUNCTION_MAP.put(\"exp10\", \"\"); // exp10(x)¶接受一个数值类型的参数并返回它的10的x次幂。\n\t\tSQL_FUNCTION_MAP.put(\"cbrt\", \"\"); // cbrt(x) 接受一个数值类型的参数并返回它的立方根。\n\t\tSQL_FUNCTION_MAP.put(\"lgamma\", \"\"); // lgamma(x) 返回x的绝对值的自然对数的伽玛函数。\n\t\tSQL_FUNCTION_MAP.put(\"tgamma\", \"\"); // tgamma(x)¶返回x的伽玛函数。\n\t\tSQL_FUNCTION_MAP.put(\"intExp2\", \"\"); // intExp2 接受一个数值类型的参数并返回它的2的x次幂（UInt64）\n\t\tSQL_FUNCTION_MAP.put(\"intExp10\", \"\"); // intExp10 接受一个数值类型的参数并返回它的10的x次幂（UInt64）。\n\t\tSQL_FUNCTION_MAP.put(\"cosh\", \"\"); // cosh(x)\n\t\tSQL_FUNCTION_MAP.put(\"sinh\", \"\"); // sinh(x)\n\t\tSQL_FUNCTION_MAP.put(\"asinh\", \"\"); // asinh(x)\n\t\tSQL_FUNCTION_MAP.put(\"atanh\", \"\"); // atanh(x)\n\t\tSQL_FUNCTION_MAP.put(\"atan2\", \"\"); // atan2(y, x)\n\t\tSQL_FUNCTION_MAP.put(\"hypot\", \"\"); // hypot(x, y)\n\t\tSQL_FUNCTION_MAP.put(\"log1p\", \"\"); // log1p(x)\n\t\tSQL_FUNCTION_MAP.put(\"trunc\", \"\"); // 和 truncate 一样\n\t\tSQL_FUNCTION_MAP.put(\"roundToExp2\", \"\"); // roundToExp2(num) 接受一个数字。如果数字小于1，它返回0。\n\t\tSQL_FUNCTION_MAP.put(\"roundDuration\", \"\"); // roundDuration(num) 接受一个数字。如果数字小于1，它返回0。\n\t\tSQL_FUNCTION_MAP.put(\"roundAge\", \"\"); // roundAge(age) 接受一个数字。如果数字小于18，它返回0。\n\t\tSQL_FUNCTION_MAP.put(\"roundDown\", \"\"); // roundDown(num, arr) 接受一个数字并将其舍入到指定数组中的一个元素\n\t\tSQL_FUNCTION_MAP.put(\"bitAnd\", \"\"); // bitAnd(a,b)\n\t\tSQL_FUNCTION_MAP.put(\"bitOr\", \"\"); // bitOr(a,b)\n\n\t\t// PostgreSQL 表结构相关 SQL 函数\n\t\tSQL_FUNCTION_MAP.put(\"obj_description\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"col_description\", \"\");\n\n\t\t// SQLServer 相关 SQL 函数\n\t\tSQL_FUNCTION_MAP.put(\"len\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"datalength\", \"\");\n\n\t\t// Milvus 相关 SQL 函数\n\t\tSQL_FUNCTION_MAP.put(\"vMatch\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"consistencyLevel\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"partitionBy\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"gracefulTime\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"guaranteeTimestamp\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"roundDecimal\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"travelTimestamp\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"nProbe\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"ef\", \"\");\n\t\tSQL_FUNCTION_MAP.put(\"searchK\", \"\");\n\n\t}\n\n\tprivate Parser<T, M, L> parser;\n\t@Override\n\tpublic Parser<T, M, L> gainParser() {\n\t\tif (parser == null && objectParser != null) {\n\t\t\tparser = objectParser.getParser();\n\t\t}\n\t\treturn parser;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setParser(Parser<T, M, L> parser) {\n\t\tthis.parser = parser;\n\t\treturn this;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> putWarnIfNeed(String type, String warn) {\n\t\tif (Log.DEBUG && parser instanceof AbstractParser) {\n\t\t\t((AbstractParser<T, M, L>) parser).putWarnIfNeed(type, warn);\n\t\t}\n\t\treturn this;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> putWarn(String type, String warn) {\n\t\tif (Log.DEBUG && parser instanceof AbstractParser) {\n\t\t\t((AbstractParser<T, M, L>) parser).putWarn(type, warn);\n\t\t}\n\t\treturn this;\n\t}\n\n\tprivate ObjectParser<T, M, L> objectParser;\n\t@Override\n\tpublic ObjectParser<T, M, L> gainObjectParser() {\n\t\treturn objectParser;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setObjectParser(ObjectParser<T, M, L> objectParser) {\n\t\tthis.objectParser = objectParser;\n\t\treturn this;\n\t}\n\n\tprivate int version;\n\t@Override\n\tpublic int getVersion() {\n\t\tif (version <= 0 && parser != null) {\n\t\t\tversion = parser.getVersion();\n\t\t}\n\t\treturn version;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setVersion(int version) {\n\t\tthis.version = version;\n\t\treturn this;\n\t}\n\n\tprivate String tag;\n\t@Override\n\tpublic String getTag() {\n\t\tif (StringUtil.isEmpty(tag) && parser != null) {\n\t\t\ttag = parser.getTag();\n\t\t}\n\t\treturn tag;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setTag(String tag) {\n\t\tthis.tag = tag;\n\t\treturn this;\n\t}\n\n\t// mysql8版本以上,子查询支持with as表达式\n\tprivate List<String> withAsExprSQLList = null;\n\tprotected List<Object> withAsExprPreparedValueList = new ArrayList<>();\n\tprivate int[] dbVersionNums = null;\n\t@Override\n\tpublic int[] gainDBVersionNums() {\n\t\tif (dbVersionNums == null || dbVersionNums.length <= 0) {\n\t\t\tdbVersionNums = SQLConfig.super.gainDBVersionNums();\n\t\t}\n\t\treturn dbVersionNums;\n\t}\n\n\t@Override\n\tpublic boolean limitSQLCount() {\n\t\treturn AbstractVerifier.SYSTEM_ACCESS_MAP.containsKey(getTable()) == false;\n\t}\n\t@Override\n\tpublic boolean allowPartialUpdateFailed() {\n\t\treturn allowPartialUpdateFailed(getTable());\n\t}\n\tpublic static boolean allowPartialUpdateFailed(String table) {\n\t\treturn ALLOW_PARTIAL_UPDATE_FAIL_TABLE_MAP.containsKey(table);\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic String getIdKey() {\n\t\treturn KEY_ID;\n\t}\n\t@NotNull\n\t@Override\n\tpublic String getUserIdKey() {\n\t\treturn KEY_USER_ID;\n\t}\n\n\tprivate RequestMethod method; //操作方法\n\tprivate boolean prepared = true; //预编译\n\tprivate boolean main = true;\n\n\tprivate Object id;  // Table 的 id\n\tprivate Object idIn;  // User Table 的 id IN\n\tprivate Object userId;  // Table 的 userId\n\tprivate Object userIdIn;  // Table 的 userId IN\n\n\t/**\n\t * TODO 被关联的表通过就忽略关联的表？(这个不行 User:{\"sex@\":\"/Comment/toId\"})\n\t */\n\tprivate String role; //发送请求的用户的角色\n\tprivate boolean distinct = false;\n\tprivate String database; //表所在的数据库类型\n\tprivate String namespace; //表所在的命名空间\n\tprivate String catalog; //表所在的目录\n\tprivate String schema; //表所在的数据库名\n\tprivate String datasource; //数据源\n\tprivate String table; //表名\n\tprivate String alias; //表别名\n\tprivate String group; //分组方式的字符串数组，','分隔\n\tprivate String havingCombine; //聚合函数的字符串数组，','分隔\n\tprivate Map<String, Object> having; //聚合函数的字符串数组，','分隔\n\tprivate String sample; //取样方式的字符串数组，','分隔\n\tprivate String latest; //最近方式的字符串数组，','分隔\n\tprivate String partition; //分区方式的字符串数组，','分隔\n\tprivate String fill; //填充方式的字符串数组，','分隔\n\tprivate String order; //排序方式的字符串数组，','分隔\n\n\tprivate Map<String, String> keyMap; //字段名映射，支持 name_tag:(name,tag) 多字段 IN，year:left(date,4) 截取日期年份等\n\tprivate List<String> raw; //需要保留原始 SQL 的字段，','分隔\n\tprivate List<String> json; //需要转为 JSON 的字段，','分隔\n\tprivate Subquery<T, M, L> from; //子查询临时表\n\tprivate List<String> column; //表内字段名(或函数名，仅查询操作可用)的字符串数组，','分隔\n\tprivate List<List<Object>> values; //对应表内字段的值的字符串数组，','分隔\n\tprivate List<String> nulls;\n\tprivate Map<String, String> cast;\n\tprivate Map<String, Object> content; //Request内容，key:value形式，column = content.keySet()，values = content.values()\n\tprivate Map<String, Object> where; //筛选条件，key:value形式\n\tprivate String combine; //条件组合， a | (b & c & !(d | !e))\n\tprivate Map<String, List<String>> combineMap; //条件组合，{ \"&\":[key], \"|\":[key], \"!\":[key] }\n\n\t//array item <<<<<<<<<<\n\tprivate int count; //Table数量\n\tprivate int page; //Table所在页码\n\tprivate int position; //Table在[]中的位置\n\tprivate int query; //apijson.JSONRequest.QUERY\n\tprivate Boolean compat; //apijson.JSONMap.compat  query total\n\tprivate int type; //ObjectParser.type\n\tprivate int cache;\n\tprivate boolean explain;\n\n\tprivate List<Join<T, M, L>> joinList; //连表 配置列表\n\t//array item >>>>>>>>>>\n\tprivate boolean test; //测试\n\n\tprivate String procedure;\n\n\tpublic AbstractSQLConfig<T, M, L> setProcedure(String procedure) {\n\t\tthis.procedure = procedure;\n\t\treturn this;\n\t}\n\tpublic String getProcedure() {\n\t\treturn procedure;\n\t}\n\n\tpublic AbstractSQLConfig(RequestMethod method) {\n\t\tsetMethod(method);\n\t}\n\tpublic AbstractSQLConfig(RequestMethod method, String table) {\n\t\tthis(method);\n\t\tsetTable(table);\n\t}\n\tpublic AbstractSQLConfig(RequestMethod method, int count, int page) {\n\t\tthis(method);\n\t\tsetCount(count);\n\t\tsetPage(page);\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic RequestMethod getMethod() {\n\t\tif (method == null) {\n\t\t\tmethod = GET;\n\t\t}\n\t\treturn method;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setMethod(RequestMethod method) {\n\t\tthis.method = method;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic boolean isPrepared() {\n\t\treturn prepared && ! isMongoDB(); // MongoDB JDBC 还不支持预编译;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setPrepared(boolean prepared) {\n\t\tthis.prepared = prepared;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic boolean isMain() {\n\t\treturn main;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setMain(boolean main) {\n\t\tthis.main = main;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic Object getId() {\n\t\treturn id;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setId(Object id) {\n\t\tthis.id = id;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Object getIdIn() {\n\t\treturn idIn;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setIdIn(Object idIn) {\n\t\tthis.idIn = idIn;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic Object getUserId() {\n\t\treturn userId;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setUserId(Object userId) {\n\t\tthis.userId = userId;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Object getUserIdIn() {\n\t\treturn userIdIn;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setUserIdIn(Object userIdIn) {\n\t\tthis.userIdIn = userIdIn;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getRole() {\n\t\t//不能 @NotNull , AbstractParser#getSQLObject 内当getRole() == null时填充默认值\n\t\treturn role;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setRole(String role) {\n\t\tthis.role = role;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic boolean isDistinct() {\n\t\treturn distinct;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setDistinct(boolean distinct) {\n\t\tthis.distinct = distinct;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getDatabase() {\n\t\treturn database;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setDatabase(String database) {\n\t\tthis.database = database;\n\t\treturn this;\n\t}\n\t/**\n\t * @return db == null ? DEFAULT_DATABASE : db\n\t */\n\t@NotNull\n\tpublic String gainSQLDatabase() {\n\t\tString db = getDatabase();\n\t\treturn db == null ? DEFAULT_DATABASE : db;  // \"\" 表示已设置，不需要用全局默认的 StringUtil.isEmpty(db, false)) {\n\t}\n\n\t@Override\n\tpublic boolean isTSQL() { // 兼容 TSQL 语法\n\t\treturn isOracle() || isSQLServer() || isDb2();\n\t}\n\t@Override\n\tpublic boolean isMSQL() { // 兼容 MySQL 语法，但不一定可以使用它的 JDBC/ODBC\n\t\treturn isMySQL() || isTiDB() || isMariaDB() || isSQLite() || isTDengine();\n\t}\n\t@Override\n\tpublic boolean isPSQL() { // 兼容 PostgreSQL 语法，但不一定可以使用它的 JDBC/ODBC\n\t\treturn isPostgreSQL() || isCockroachDB() || isOpenGauss() || isInfluxDB() || isTimescaleDB() || isQuestDB() || isDuckDB();\n\t}\n\n\t@Override\n\tpublic boolean isMySQL() {\n\t\treturn isMySQL(gainSQLDatabase());\n\t}\n\tpublic static boolean isMySQL(String db) {\n\t\treturn DATABASE_MYSQL.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isPostgreSQL() {\n\t\treturn isPostgreSQL(gainSQLDatabase());\n\t}\n\tpublic static boolean isPostgreSQL(String db) {\n\t\treturn DATABASE_POSTGRESQL.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isSQLServer() {\n\t\treturn isSQLServer(gainSQLDatabase());\n\t}\n\tpublic static boolean isSQLServer(String db) {\n\t\treturn DATABASE_SQLSERVER.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isOracle() {\n\t\treturn isOracle(gainSQLDatabase());\n\t}\n\tpublic static boolean isOracle(String db) {\n\t\treturn DATABASE_ORACLE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDb2() {\n\t\treturn isDb2(gainSQLDatabase());\n\t}\n\tpublic static boolean isDb2(String db) {\n\t\treturn DATABASE_DB2.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isMariaDB() {\n\t\treturn isMariaDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isMariaDB(String db) {\n\t\treturn DATABASE_MARIADB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isTiDB() {\n\t\treturn isTiDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isTiDB(String db) {\n\t\treturn DATABASE_TIDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isCockroachDB() {\n\t\treturn isCockroachDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isCockroachDB(String db) {\n\t\treturn DATABASE_COCKROACHDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDameng() {\n\t\treturn isDameng(gainSQLDatabase());\n\t}\n\tpublic static boolean isDameng(String db) {\n\t\treturn DATABASE_DAMENG.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isKingBase() {\n\t\treturn isKingBase(gainSQLDatabase());\n\t}\n\tpublic static boolean isKingBase(String db) {\n\t\treturn DATABASE_KINGBASE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isElasticsearch() {\n\t\treturn isElasticsearch(gainSQLDatabase());\n\t}\n\tpublic static boolean isElasticsearch(String db) {\n\t\treturn DATABASE_ELASTICSEARCH.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isManticore() {\n\t\treturn isManticore(gainSQLDatabase());\n\t}\n\tpublic static boolean isManticore(String db) {\n\t\treturn DATABASE_MANTICORE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isClickHouse() {\n\t\treturn isClickHouse(gainSQLDatabase());\n\t}\n\tpublic static boolean isClickHouse(String db) {\n\t\treturn DATABASE_CLICKHOUSE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isHive() {\n\t\treturn isHive(gainSQLDatabase());\n\t}\n\tpublic static boolean isHive(String db) {\n\t\treturn DATABASE_HIVE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isPresto() {\n\t\treturn isPresto(gainSQLDatabase());\n\t}\n\tpublic static boolean isPresto(String db) {\n\t\treturn DATABASE_PRESTO.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isTrino() {\n\t\treturn isTrino(gainSQLDatabase());\n\t}\n\tpublic static boolean isTrino(String db) {\n\t\treturn DATABASE_TRINO.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isSnowflake() {\n\t\treturn isSnowflake(gainSQLDatabase());\n\t}\n\tpublic static boolean isSnowflake(String db) {\n\t\treturn DATABASE_SNOWFLAKE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDatabend() {\n\t\treturn isDatabend(gainSQLDatabase());\n\t}\n\tpublic static boolean isDatabend(String db) {\n\t\treturn DATABASE_DATABEND.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDatabricks() {\n\t\treturn isDatabricks(gainSQLDatabase());\n\t}\n\tpublic static boolean isDatabricks(String db) {\n\t\treturn DATABASE_DATABRICKS.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isCassandra() {\n\t\treturn isCassandra(gainSQLDatabase());\n\t}\n\tpublic static boolean isCassandra(String db) {\n\t\treturn DATABASE_CASSANDRA.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isMilvus() {\n\t\treturn isMilvus(gainSQLDatabase());\n\t}\n\tpublic static boolean isMilvus(String db) {\n\t\treturn DATABASE_MILVUS.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isInfluxDB() {\n\t\treturn isInfluxDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isInfluxDB(String db) {\n\t\treturn DATABASE_INFLUXDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isTDengine() {\n\t\treturn isTDengine(gainSQLDatabase());\n\t}\n\tpublic static boolean isTDengine(String db) {\n\t\treturn DATABASE_TDENGINE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isTimescaleDB() {\n\t\treturn isTimescaleDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isTimescaleDB(String db) {\n\t\treturn DATABASE_TIMESCALEDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isQuestDB() {\n\t\treturn isQuestDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isQuestDB(String db) {\n\t\treturn DATABASE_QUESTDB.equals(db);\n\t}\n\n\n\tpublic boolean isIoTDB() {\n\t\treturn isIoTDB(getDatabase());\n\t}\n\tpublic static boolean isIoTDB(String db) {\n\t\treturn DATABASE_IOTDB.equals(db);\n\t}\n\n\n\t@Override\n\tpublic boolean isRedis() {\n\t\treturn isRedis(gainSQLDatabase());\n\t}\n\tpublic static boolean isRedis(String db) {\n\t\treturn DATABASE_REDIS.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isMongoDB() {\n\t\treturn isMongoDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isMongoDB(String db) {\n\t\treturn DATABASE_MONGODB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isKafka() {\n\t\treturn isKafka(gainSQLDatabase());\n\t}\n\tpublic static boolean isKafka(String db) {\n\t\treturn DATABASE_KAFKA.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isMQ() {\n\t\treturn isMQ(gainSQLDatabase());\n\t}\n\tpublic static boolean isMQ(String db) {\n\t\treturn DATABASE_MQ.equals(db) || isKafka(db);\n\t}\n\n\t@Override\n\tpublic boolean isSQLite() {\n\t\treturn isSQLite(gainSQLDatabase());\n\t}\n\tpublic static boolean isSQLite(String db) {\n\t\treturn DATABASE_SQLITE.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDuckDB() {\n\t\treturn isDuckDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isDuckDB(String db) {\n\t\treturn DATABASE_DUCKDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isSurrealDB() {\n\t\treturn isSurrealDB(gainSQLDatabase());\n\t}\n\tpublic static boolean isSurrealDB(String db) {\n\t\treturn DATABASE_SURREALDB.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isOpenGauss() {\n\t\treturn isOpenGauss(gainSQLDatabase());\n\t}\n\tpublic static boolean isOpenGauss(String db) {\n\t\treturn DATABASE_OPENGAUSS.equals(db);\n\t}\n\n\t@Override\n\tpublic boolean isDoris() {\n\t\treturn isDoris(gainSQLDatabase());\n\t}\n\tpublic static boolean isDoris(String db) {\n\t\treturn DATABASE_DORIS.equals(db);\n\t}\n\n\t@Override\n\tpublic String getQuote() { // MongoDB  同时支持 `tbl` 反引号 和 \"col\" 双引号\n\t\tif(isElasticsearch() || isManticore() || isIoTDB() || isSurrealDB()) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn isMySQL() || isMariaDB() || isTiDB() || isClickHouse() || isTDengine() || isMilvus() || isDoris() ? \"`\" : \"\\\"\";\n\t}\n\n\tpublic String quote(String s) {\n\t\tString q = getQuote();\n\t\treturn q + s + q;\n\t}\n\n\t@Override\n\tpublic String getSQLNamespace() {\n\t\tString sch = getNamespace(); // 前端传参 @namespace 优先\n\t\treturn sch == null ? DEFAULT_NAMESPACE : sch; // 最后代码默认兜底配置\n\t}\n\n\t@Override\n\tpublic String getNamespace() {\n\t\treturn namespace;\n\t}\n\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setNamespace(String namespace) {\n\t\tthis.namespace = namespace;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic String gainSQLCatalog() {\n\t\tString catalog = getCatalog(); // 前端传参 @catalog 优先\n\t\treturn catalog == null ? DEFAULT_CATALOG : catalog; // 最后代码默认兜底配置\n\t}\n\n\t@Override\n\tpublic String getCatalog() {\n\t\treturn catalog;\n\t}\n\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCatalog(String catalog) {\n\t\tthis.catalog = catalog;\n\t\treturn this;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic String gainSQLSchema() {\n\t\tString table = getTable();\n\t\t// FIXME 全部默认填充判断是 系统表 则不填充 // 强制，避免因为全局默认的 @schema 自动填充进来，导致这几个类的 schema 为 sys 等其它值\n\t\tif (Table.TAG.equals(table) || Column.TAG.equals(table)) {\n\t\t\treturn SCHEMA_INFORMATION; //MySQL, PostgreSQL, SQL Server 都有的\n\t\t}\n\t\tif (PgClass.TAG.equals(table) || PgAttribute.TAG.equals(table)) {\n\t\t\treturn \"\"; //PostgreSQL 的 pg_class 和 pg_attribute 表好像不属于任何 Schema\n\t\t}\n\t\tif (SysTable.TAG.equals(table) || SysColumn.TAG.equals(table) || ExtendedProperty.TAG.equals(table)) {\n\t\t\treturn SCHEMA_SYS; //SQL Server 在 sys 中的属性比 information_schema 中的要全，能拿到注释\n\t\t}\n\t\tif (AllTable.TAG.equals(table) || AllColumn.TAG.equals(table)\n\t\t\t\t|| AllTableComment.TAG.equals(table) || AllColumnComment.TAG.equals(table)) {\n\t\t\treturn \"\"; //Oracle, Dameng 的 all_tables, dba_tables 和 all_tab_columns, dba_columns 表好像不属于任何 Schema\n\t\t}\n\n\t\tString sch = getSchema(); // 前端传参 @schema 优先\n\t\tif (sch == null) {\n\t\t\tsch = TABLE_SCHEMA_MAP.get(table); // 其次 Access 表 alias 和 schema 配置\n\t\t}\n\t\treturn sch == null ? DEFAULT_SCHEMA : sch; // 最后代码默认兜底配置\n\t}\n\n\t@Override\n\tpublic String getSchema() {\n\t\treturn schema;\n\t}\n\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setSchema(String schema) {\n\t\tif (schema != null) {\n\t\t\tAbstractFunctionParser.verifySchema(schema, getTable());\n\t\t}\n\t\tthis.schema = schema;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic String getDatasource() {\n\t\treturn datasource;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setDatasource(String datasource) {\n\t\tthis.datasource = datasource;\n\t\treturn this;\n\t}\n\n\t/**请求传进来的Table名\n\t * @return\n\t * @see {@link #gainSQLTable()}\n\t */\n\t@Override\n\tpublic String getTable() {\n\t\treturn table;\n\t}\n\t/**数据库里的真实Table名\n\t * 通过 {@link #TABLE_KEY_MAP} 映射\n\t * @return\n\t */\n\t@Override\n\tpublic String gainSQLTable() {\n\t\t// 如果要强制小写，则可在子类重写这个方法再 toLowerCase\n\t\t// return DATABASE_POSTGRESQL.equals(getDatabase()) ? t.toLowerCase() : t;\n\t\tString ot = getTable();\n\t\tString nt = TABLE_KEY_MAP.get(ot);\n\t\treturn StringUtil.isEmpty(nt) ? ot : nt;\n\t}\n\n\n\t@Override\n\tpublic String gainTablePath() {\n\t\tString q = getQuote();\n\n\t\tString ns = isSurrealDB() ? getSQLNamespace() : null;\n\t\tString cl = isPSQL() ? gainSQLCatalog() : null;\n\t\tString sch = gainSQLSchema();\n\t\tString sqlTable = gainSQLTable();\n\n\t\treturn (StringUtil.isEmpty(ns, true) ? \"\" : q + ns + q + \".\")\n\t\t\t\t+ (StringUtil.isEmpty(cl, true) ? \"\" : q + cl + q + \".\")\n\t\t\t\t+ (StringUtil.isEmpty(sch, true) ? \"\" : q + sch + q + \".\")\n\t\t\t\t+ q + sqlTable + q + (isKeyPrefix() ? gainAs() + q + gainSQLAlias() + q : \"\");\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setTable(String table) { //Table已经在Parser中校验，所以这里不用防SQL注入\n\t\tthis.table = table;\n\t\treturn this;\n\t}\n\n\tpublic String gainAs() {\n\t\treturn isOracle() || isManticore() ? \" \" : \" AS \";\n\t}\n\n\t@Override\n\tpublic String getAlias() {\n\t\treturn alias;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setAlias(String alias) {\n\t\tthis.alias = alias;\n\t\treturn this;\n\t}\n\tpublic String gainSQLAliasWithQuote() {\n\t\tString a = gainSQLAlias();\n\t\tString q = getQuote();\n\t\t// getTable 不能小写，因为Verifier用大小写敏感的名称判断权限\n\t\t// 如果要强制小写，则可在子类重写这个方法再 toLowerCase\n\t\t// return q + (DATABASE_POSTGRESQL.equals(getDatabase()) ? a.toLowerCase() : a) + q;\n\t\treturn q + a + q;\n\t}\n\n\t@Override\n\tpublic String getGroup() {\n\t\treturn group;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setGroup(String... keys) {\n\t\treturn setGroup(StringUtil.get(keys));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setGroup(String group) {\n\t\tthis.group = group;\n\t\treturn this;\n\t}\n\n\tpublic String gainGroupString(boolean hasPrefix) {\n\t\t//加上子表的 group\n\t\tString joinGroup = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getGroup() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t//if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\tcfg.setAlias(cfg.getTable());\n\t\t\t\t\t//}\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainGroupString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinGroup += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t////先处理左/右关联，内关联忽略\n\t\t\t\t//SQLConfig<T, M, L> outerConfig = join.getOuterConfig();\n\t\t\t\t//SQLConfig<T, M, L> outerConfig2 = (outerConfig != null && outerConfig.getGroup() != null) || join.isLeftOrRightJoin() ? outerConfig : null;\n\t\t\t\t//\n\t\t\t\t//if (outerConfig2 != null) {\n\t\t\t\t//\touterConfig2.setMain(false).setKeyPrefix(true);\n\t\t\t\t//\t//if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t//\t//\tcfg.setAlias(cfg.getTable());\n\t\t\t\t//\t//}\n\t\t\t\t//\tString c = ((AbstractSQLConfig<?, ?, ?>) outerConfig2).gainGroupString(false);\n\t\t\t\t//\n\t\t\t\t//\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t//\t\tjoinGroup += (first ? \"\" : \", \") + c;\n\t\t\t\t//\t\tfirst = false;\n\t\t\t\t//\t}\n\t\t\t\t//}\n\t\t\t}\n\t\t}\n\n\n\t\tgroup = StringUtil.trim(group);\n\t\tString[] keys = StringUtil.split(group);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinGroup, true) ? \"\" : (hasPrefix ? \" GROUP BY \" : \"\") + joinGroup;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tif (isPrepared()) {\n\t\t\t\t// 不能通过 ? 来代替，因为SQLExecutor<T, M, L> statement.setString后 GROUP BY 'userId' 有单引号，只能返回一条数据，必须去掉单引号才行！\n\t\t\t\tif (StringUtil.isName(keys[i]) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"@group:value 中 value里面用 , 分割的每一项都必须是1个单词！并且不要有空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(keys[i]);\n\t\t}\n\n\t\treturn (hasPrefix ? \" GROUP BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinGroup, \", \");\n\t}\n\n\t@Override\n\tpublic String getHavingCombine() {\n\t\treturn havingCombine;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setHavingCombine(String havingCombine) {\n\t\tthis.havingCombine = havingCombine;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Map<String, Object> getHaving() {\n\t\treturn having;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setHaving(Map<String, Object> having) {\n\t\tthis.having = having;\n\t\treturn this;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setHaving(String... conditions) {\n\t\treturn setHaving(StringUtil.get(conditions));\n\t}\n\n\t/**TODO @having 改为默认 | 或连接，且支持 @having: { \"key1>\": 1, \"key{}\": \"length(key2)>0\", \"@combine\": \"key1,key2\" }\n\t * @return HAVING conditoin0 AND condition1 OR condition2 ...\n\t * @throws Exception\n\t */\n\tpublic String gainHavingString(boolean hasPrefix) throws Exception {\n\t\t//加上子表的 having\n\t\tString joinHaving = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getHaving() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t//if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\tcfg.setAlias(cfg.getTable());\n\t\t\t\t\t//}\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainHavingString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinHaving += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMap<String, Object> map = getHaving();\n\t\tSet<Entry<String, Object>> set = map == null ? null : map.entrySet();\n\t\tif (set == null || set.isEmpty()) {\n\t\t\treturn StringUtil.isEmpty(joinHaving, true) ? \"\" : (hasPrefix ? \" HAVING \" : \"\") + joinHaving;\n\t\t}\n\n\t\tList<String> raw = getRaw();\n\t\t//\t提前把 @having& 转为 @having，或者干脆不允许 @raw:\"@having&\"\tboolean containRaw = raw != null && (raw.contains(KEY_HAVING) || raw.contains(KEY_HAVING_AND));\n\t\tboolean containRaw = raw != null && raw.contains(KEY_HAVING);\n\n\t\t// 直接把 having 类型从 Map<String, String> 定改为 Map<String, Object>，避免额外拷贝\n\t\t//\t\tMap<String, Object> newMap = new LinkedHashMap<>(map.size());\n\t\t//\t\tfor (Entry<String, String> entry : set) {\n\t\t//\t\t\tnewMap.put(entry.getKey(), entry.getValue());\n\t\t//\t\t}\n\n\t\t//fun0(arg0,arg1,...);fun1(arg0,arg1,...)\n\t\tString havingString = parseCombineExpression(getMethod(), getQuote(), getTable()\n\t\t\t\t, getAlias(), map, getHavingCombine(), true, containRaw, true);\n\n\t\treturn (hasPrefix ? \" HAVING \" : \"\") + StringUtil.concat(havingString, joinHaving, AND);\n\t}\n\n\tprotected String gainHavingItem(String quote, String table, String alias\n\t\t\t, String key, String expression, boolean containRaw) throws Exception {\n\t\t//fun(arg0,arg1,...)\n\t\tif (containRaw) {\n\t\t\tString rawSQL = gainRawSQL(KEY_HAVING, expression);\n\t\t\tif (rawSQL != null) {\n\t\t\t\treturn rawSQL;\n\t\t\t}\n\t\t}\n\n\t\tif (expression.length() > 100) {\n\t\t\tthrow new UnsupportedOperationException(\"@having:value 的 value 中字符串 \" + expression + \" 不合法！\"\n\t\t\t\t\t+ \"不允许传超过 100 个字符的函数或表达式！请用 @raw 简化传参！\");\n\t\t}\n\n\t\tint start = expression.indexOf(\"(\");\n\t\tif (start < 0) {\n\t\t\tif (isPrepared() && PATTERN_FUNCTION.matcher(expression).matches() == false) {\n\t\t\t\tthrow new UnsupportedOperationException(\"字符串 \" + expression + \" 不合法！\"\n\t\t\t\t\t\t+ \"预编译模式下 @having:\\\"column?value;function(arg0,arg1,...)?value...\\\"\"\n\t\t\t\t\t\t+ \" 中 column?value 必须符合正则表达式 \" + PATTERN_FUNCTION + \" 且不包含连续减号 -- ！不允许空格！\");\n\t\t\t}\n\t\t\treturn expression;\n\t\t}\n\n\t\tint end = expression.lastIndexOf(\")\");\n\t\tif (start >= end) {\n\t\t\tthrow new IllegalArgumentException(\"字符 \" + expression + \" 不合法！\"\n\t\t\t\t\t+ \"@having:value 中 value 里的 SQL函数必须为 function(arg0,arg1,...) 这种格式！\");\n\t\t}\n\n\t\tString method = expression.substring(0, start);\n\t\tif (method.isEmpty() == false) {\n\t\t\tif (SQL_FUNCTION_MAP == null || SQL_FUNCTION_MAP.isEmpty()) {\n\t\t\t\tif (StringUtil.isName(method) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + method + \" 不合法！\"\n\t\t\t\t\t\t\t+ \"预编译模式下 @having:\\\"column?value;function(arg0,arg1,...)?value...\\\"\"\n\t\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (SQL_FUNCTION_MAP.containsKey(method) == false) {\n\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + method + \" 不合法！\"\n\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"column0,column1:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！且必须是后端允许调用的 SQL 函数!\");\n\t\t\t}\n\t\t}\n\n\t\treturn method + parseSQLExpression(KEY_HAVING, expression.substring(start), containRaw, false, null);\n\t}\n\n\t@Override\n\tpublic String getSample() {\n\t\treturn sample;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setSample(String... conditions) {\n\t\treturn setSample(StringUtil.get(conditions));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setSample(String sample) {\n\t\tthis.sample = sample;\n\t\treturn this;\n\t}\n\tpublic String gainSampleString(boolean hasPrefix) {\n\t\t//加上子表的 sample\n\t\tString joinSample = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getSample() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t// if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\t cfg.setAlias(cfg.getTable());\n\t\t\t\t\t// }\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainSampleString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinSample += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tString sample = StringUtil.trim(getSample());\n\n\t\tString[] keys = StringUtil.split(sample);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinSample, true) ? \"\" : (hasPrefix ? \" SAMPLE BY \" : \"\") + joinSample;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\n\t\t\tString origin = item;\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin)) {}\n\t\t\t\telse if (StringUtil.isCombineOfNumOrAlpha(origin)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @sample:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 column 且其中 column 必须是 数字或英语字母组合！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin);\n\t\t}\n\n\t\treturn (hasPrefix ? \" SAMPLE BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinSample, \", \");\n\t}\n\n\t@Override\n\tpublic String getLatest() {\n\t\treturn latest;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setLatest(String... conditions) {\n\t\treturn setLatest(StringUtil.get(conditions));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setLatest(String latest) {\n\t\tthis.latest = latest;\n\t\treturn this;\n\t}\n\tpublic String gainLatestString(boolean hasPrefix) {\n\t\t//加上子表的 latest\n\t\tString joinLatest = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getLatest() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t// if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\t cfg.setAlias(cfg.getTable());\n\t\t\t\t\t// }\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainLatestString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinLatest += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tString latest = StringUtil.trim(getLatest());\n\n\t\tString[] keys = StringUtil.split(latest);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinLatest, true) ? \"\" : (hasPrefix ? \" LATEST ON \" : \"\") + joinLatest;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\t\t\tString origin = item;\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @latest:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 column 且其中 column 必须是 英语单词！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin);\n\t\t}\n\n\t\treturn (hasPrefix ? \" LATEST ON \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinLatest, \", \");\n\t}\n\n\t@Override\n\tpublic String getPartition() {\n\t\treturn partition;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setPartition(String... conditions) {\n\t\treturn setPartition(StringUtil.get(conditions));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setPartition(String partition) {\n\t\tthis.partition = partition;\n\t\treturn this;\n\t}\n\tpublic String gainPartitionString(boolean hasPrefix) {\n\t\t//加上子表的 partition\n\t\tString joinPartition = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getPartition() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t// if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\t cfg.setAlias(cfg.getTable());\n\t\t\t\t\t// }\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainPartitionString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinPartition += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tString partition = StringUtil.trim(getPartition());\n\n\t\tString[] keys = StringUtil.split(partition);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinPartition, true) ? \"\" : (hasPrefix ? \" PARTITION BY \" : \"\") + joinPartition;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\t\t\tString origin = item;\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @partition:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 column 且其中 column 必须是 英语单词！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin);\n\t\t}\n\n\t\treturn (hasPrefix ? \" PARTITION BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinPartition, \", \");\n\t}\n\n\t@Override\n\tpublic String getFill() {\n\t\treturn fill;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setFill(String... conditions) {\n\t\treturn setFill(StringUtil.get(conditions));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setFill(String fill) {\n\t\tthis.fill = fill;\n\t\treturn this;\n\t}\n\tpublic String gainFillString(boolean hasPrefix) {\n\t\t//加上子表的 fill\n\t\tString joinFill = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getFill() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t// if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\t cfg.setAlias(cfg.getTable());\n\t\t\t\t\t// }\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainFillString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinFill += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tString fill = StringUtil.trim(getFill());\n\n\t\tString[] keys = StringUtil.split(fill);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinFill, true) ? \"\" : (hasPrefix ? \" FILL(\" : \"\") + joinFill + \")\";\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\t\t\tif (\"NULL\".equals(item) || \"LINEAR\".equals(item) || \"PREV\".equals(item) || \"PREVIOUS\".equals(item)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tString origin = item;\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin)) {}\n\t\t\t\telse if (StringUtil.isCombineOfNumOrAlpha(origin)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @fill:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 column 且其中 column 必须是 数字或英语字母组合！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin);\n\t\t}\n\n\t\treturn (hasPrefix ? \" FILL(\" : \"\") + StringUtil.concat(StringUtil.get(keys), joinFill, \", \") + \")\";\n\t}\n\n\t@Override\n\tpublic String getOrder() {\n\t\treturn order;\n\t}\n\tpublic AbstractSQLConfig<T, M, L> setOrder(String... conditions) {\n\t\treturn setOrder(StringUtil.get(conditions));\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setOrder(String order) {\n\t\tthis.order = order;\n\t\treturn this;\n\t}\n\tpublic String gainOrderString(boolean hasPrefix) {\n\t\t//加上子表的 order\n\t\tString joinOrder = \"\";\n\t\tString joinOuterOrder = \"\";\n\t\tif (joinList != null) {\n\t\t\tboolean first = true;\n\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\tSQLConfig<T, M, L> cfg = (ocfg != null && ocfg.getOrder() != null) || join.isLeftOrRightJoin() ? ocfg : join.getJoinConfig();\n\n\t\t\t\tif (cfg != null) {\n\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t//if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t//\tcfg.setAlias(cfg.getTable());\n\t\t\t\t\t//}\n\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainOrderString(false);\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\tjoinOrder += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\n\t\tString order = StringUtil.trim(getOrder());\n\t\t// SELECT * FROM sys.Moment ORDER BY userId ASC, rand();   前面的 userId ASC 和后面的 rand() 都有效\n\t\t//\t\tif (\"rand()\".equals(order)) {\n\t\t//\t\t\treturn (hasPrefix ? \" ORDER BY \" : \"\") + StringUtil.concat(order, joinOrder, \", \");\n\t\t//\t\t}\n\n\t\tif (getCount() > 0 && (isSQLServer() || isDb2())) {\n\t\t\t// Oracle, SQL Server, DB2 的 OFFSET 必须加 ORDER BY.去掉Oracle，Oracle里面没有offset关键字\n\n\t\t\t//\t\t\tString[] ss = StringUtil.split(order);\n\t\t\tif (StringUtil.isEmpty(order, true)) {  //SQL Server 子查询内必须指定 OFFSET 才能用 ORDER BY\n\t\t\t\tString idKey = getIdKey();\n\t\t\t\tif (StringUtil.isEmpty(idKey, true)) {\n\t\t\t\t\tidKey = \"id\";\n\t\t\t\t\t// ORDER BY NULL 不行，SQL Server 会报错，必须要有排序，才能使用 OFFSET FETCH，如果没有 idKey，请求中指定 @order 即可\n\t\t\t\t}\n\t\t\t\torder = idKey; //让数据库调控默认升序还是降序  + \"+\";\n\t\t\t}\n\n\t\t\t//不用这么全面，毕竟没有语法问题还浪费性能，如果有其它问题，让前端传的 JSON 直接加上 @order 来解决\n\t\t\t//\t\t\tboolean contains = false;\n\t\t\t//\t\t\tif (ss != null) {\n\t\t\t//\t\t\t\tfor (String s : ss) {\n\t\t\t//\t\t\t\t\tif (s != null && s.startsWith(idKey)) {\n\t\t\t//\t\t\t\t\t\ts = s.substring(idKey.length());\n\t\t\t//\t\t\t\t\t\tif (\"+\".equals(s) || \"-\".equals(s)) {// || \" ASC \".equals(s) || \" DESC \".equals(s)) {\n\t\t\t//\t\t\t\t\t\t\tcontains = true;\n\t\t\t//\t\t\t\t\t\t\tbreak;\n\t\t\t//\t\t\t\t\t\t}\n\t\t\t//\t\t\t\t\t}\n\t\t\t//\t\t\t\t}\n\t\t\t//\t\t\t}\n\n\t\t\t//\t\t\tif (contains == false) {\n\t\t\t//\t\t\t\torder = (ss == null || ss.length <= 0 ? \"\" : order + \",\") + idKey + \"+\";\n\t\t\t//\t\t\t}\n\t\t}\n\n\n\t\tString[] keys = StringUtil.split(order);\n\t\tif (keys == null || keys.length <= 0) {\n\t\t\treturn StringUtil.isEmpty(joinOrder, true) ? \"\" : (hasPrefix ? \" ORDER BY \" : \"\") + joinOrder;\n\t\t}\n\n\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\tString item = keys[i];\n\t\t\tif (\"rand()\".equals(item)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tint index = item.endsWith(\"+\") ? item.length() - 1 : -1; //StringUtil.split返回数组中，子项不会有null\n\t\t\tString sort;\n\t\t\tif (index < 0) {\n\t\t\t\tindex = item.endsWith(\"-\") ? item.length() - 1 : -1;\n\t\t\t\tsort = index <= 0 ? \"\" : \" DESC \";\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsort = \" ASC \";\n\t\t\t}\n\n\t\t\tString origin = index < 0 ? item : item.substring(0, index);\n\n\t\t\tif (isPrepared()) { //不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t//这里既不对origin trim，也不对 ASC/DESC ignoreCase，希望前端严格传没有任何空格的字符串过来，减少传输数据量，节约服务器性能\n\t\t\t\tif (StringUtil.isName(origin) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"预编译模式下 @order:value 中 \" + item + \" 不合法! value 里面用 , 分割的\"\n\t\t\t\t\t\t\t+ \"每一项必须是 随机函数 rand() 或 column+ / column- 且其中 column 必须是 1 个单词！并且不要有多余的空格！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkeys[i] = gainKey(origin) + sort;\n\t\t}\n\n\t\treturn (hasPrefix ? \" ORDER BY \" : \"\") + StringUtil.concat(StringUtil.get(keys), joinOrder, \", \");\n\t}\n\n\t@Override\n\tpublic Map<String, String> getKeyMap() {\n\t\treturn keyMap;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setKeyMap(Map<String, String> keyMap) {\n\t\tthis.keyMap = keyMap;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic List<String> getRaw() {\n\t\treturn raw;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setRaw(List<String> raw) {\n\t\tthis.raw = raw;\n\t\treturn this;\n\t}\n\n\t/**获取原始 SQL 片段\n\t * @param key\n\t * @param value\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic String gainRawSQL(String key, Object value) throws Exception {\n\t\treturn gainRawSQL(key, value, ! ALLOW_MISSING_KEY_4_COMBINE);\n\t}\n\t/**获取原始 SQL 片段\n\t * @param key\n\t * @param value\n\t * @param throwWhenMissing\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic String gainRawSQL(String key, Object value, boolean throwWhenMissing) throws Exception {\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tList<String> rawList = getRaw();\n\t\tboolean containRaw = rawList != null && rawList.contains(key);\n\t\tif (containRaw && value instanceof String == false) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 的 value 中 \" + key + \" 不合法！\"\n\t\t\t\t\t+ \"对应的 \" + key + \":value 中 value 类型只能为 String！\");\n\t\t}\n\n\t\tString rawSQL = containRaw ? RAW_MAP.get(value) : null;\n\t\tif (containRaw) {\n\t\t\tif (rawSQL == null) {\n\t\t\t\tif (throwWhenMissing) {\n\t\t\t\t\tthrow new UnsupportedOperationException(\"@raw:value 的 value 中 \" + key + \" 不合法！\"\n\t\t\t\t\t\t\t+ \"对应的 \" + key + \":value 中 value 值 \" + value + \" 未在后端 RAW_MAP 中配置 ！\");\n\t\t\t\t}\n\n\t\t\t\tputWarnIfNeed(JSONMap.KEY_RAW, \"@raw:value 的 value 中 \"\n\t\t\t\t\t\t\t+ key + \" 不合法！对应的 \" + key + \":value 中 value 值 \" + value + \" 未在后端 RAW_MAP 中配置 ！\");\n\t\t\t}\n\t\t\telse if (rawSQL.isEmpty()) {\n\t\t\t\treturn (String) value;\n\t\t\t}\n\t\t}\n\n\t\treturn rawSQL;\n\t}\n\n\n\t@Override\n\tpublic List<String> getJson() {\n\t\treturn json;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setJson(List<String> json) {\n\t\tthis.json = json;\n\t\treturn this;\n\t}\n\n\n\t@Override\n\tpublic Subquery<T, M, L> getFrom() {\n\t\treturn from;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setFrom(Subquery<T, M, L> from) {\n\t\tthis.from = from;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic List<String> getColumn() {\n\t\treturn column;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setColumn(List<String> column) {\n\t\tthis.column = column;\n\t\treturn this;\n\t}\n\tpublic String gainColumnString() throws Exception {\n\t\treturn gainColumnString(false);\n\t}\n\tpublic String gainColumnString(boolean inSQLJoin) throws Exception {\n\t\tList<String> column = getColumn();\n\t\tString as = gainAs();\n\t\tString q = getQuote();\n\n\t\tswitch (getMethod()) {\n\t\tcase HEAD:\n\t\tcase HEADS: //StringUtil.isEmpty(column, true) || column.contains(\",\") 时SQL.count(column)会return \"*\"\n\t\t\tif (isPrepared() && column != null) {\n\t\t\t\tList<String> raw = getRaw();\n\t\t\t\tboolean containRaw = raw != null && raw.contains(KEY_COLUMN);\n\n\t\t\t\tfor (String c : column) {\n\t\t\t\t\tif (containRaw) {\n\t\t\t\t\t\t// 由于 HashMap 对 key 做了 hash 处理，所以 get 比 containsValue 更快\n\t\t\t\t\t\tif (\"\".equals(RAW_MAP.get(c)) || RAW_MAP.containsValue(c)) {  // newSQLConfig<T, M, L> 提前处理好的\n\t\t\t\t\t\t\t//排除@raw中的值，以避免使用date_format(date,'%Y-%m-%d %H:%i:%s') 时,冒号的解析出错\n\t\t\t\t\t\t\t//column.remove(c);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tint index = c.lastIndexOf(\":\"); //StringUtil.split返回数组中，子项不会有null\n\t\t\t\t\tString origin = index < 0 ? c : c.substring(0, index);\n\t\t\t\t\tString alias = index < 0 ? null : c.substring(index + 1);\n\n\t\t\t\t\tif (alias != null && StringUtil.isName(alias) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"HEAD请求: 字符 \" + alias\n\t\t\t\t\t\t\t\t+ \" 不合法！预编译模式下 @column:value 中 value里面用 , 分割的每一项\"\n\t\t\t\t\t\t\t\t+ \" column:alias 中 column 必须是1个单词！如果有alias，则alias也必须为1个单词！并且不要有多余的空格！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (StringUtil.isName(origin) == false) {\n\t\t\t\t\t\tint start = origin.indexOf(\"(\");\n\t\t\t\t\t\tif (start < 0 || origin.lastIndexOf(\")\") <= start) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"HEAD请求: 字符\" + origin\n\t\t\t\t\t\t\t\t\t+ \" 不合法！预编译模式下 @column:value 中 value里面用 , 分割的每一项\"\n\t\t\t\t\t\t\t\t\t+ \" column:alias 中 column 必须是1个单词！\"\n\t\t\t\t\t\t\t\t\t+ \"如果有alias，则 alias 也必须为1个单词！并且不要有多余的空格！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (start > 0 && StringUtil.isName(origin.substring(0, start)) == false) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"HEAD请求: 字符 \" + origin.substring(0, start)\n\t\t\t\t\t\t\t\t\t+ \" 不合法！预编译模式下 @column:value 中 value里面用 , 分割的每一项\"\n\t\t\t\t\t\t\t\t\t+ \" column:alias 中 column 必须是1个单词！如果有alias，则alias也必须为1个单词！并且不要有多余的空格！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tboolean onlyOne = column != null && column.size() == 1;\n\t\t\tString c0 = onlyOne ? column.get(0) : null;\n\n\t\t\tif (onlyOne) {\n\t\t\t\tint index = c0 == null ? -1 : c0.lastIndexOf(\":\");\n\t\t\t\tif (index > 0) {\n\t\t\t\t\tc0 = c0.substring(0, index);\n\t\t\t\t}\n\n\t\t\t\tint start = c0 == null ? -1 : c0.indexOf(\"(\");\n\t\t\t\tint end = start <= 0 ? -1 : c0.lastIndexOf(\")\");\n\t\t\t\tif (start > 0 && end > start) {\n\t\t\t\t\tString fun = c0.substring(0, start);\n\n\t\t\t\t\t// Invalid use of group function  SELECT count(max(`id`))  AS count  FROM `sys`.`Comment`\n\t\t\t\t\tif (SQL_AGGREGATE_FUNCTION_MAP.containsKey(fun)) {\n\t\t\t\t\t\tString group = getGroup();  // TODO 唯一 100% 兼容的可能只有 SELECT count(*) FROM (原语句) AS table\n\t\t\t\t\t\treturn StringUtil.isEmpty(group, true) ? \"1\" : \"count(DISTINCT \" + group + \")\";\n\t\t\t\t\t}\n\n\t\t\t\t\tString[] args = start == end - 1 ? null : StringUtil.split(c0.substring(start + 1, end));\n\t\t\t\t\tif (args != null && args.length > 0) {\n\t\t\t\t\t\tList<String> raw = getRaw();\n\t\t\t\t\t\tboolean containRaw = raw != null && raw.contains(KEY_COLUMN);\n\t\t\t\t\t\tc0 = parseSQLExpression(KEY_COLUMN, c0, containRaw, false, null);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn \"count(\" + c0 + \")\" + as + q + JSONResponse.KEY_COUNT + q;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn \"count(\" + (onlyOne ? gainKey(c0) : \"*\") + \")\" + as + q + JSONResponse.KEY_COUNT + q;\n\t\t\t//\t\t\treturn SQL.count(onlyOne && StringUtil.isName(column.get(0)) ? getKey(column.get(0)) : \"*\");\n\t\tcase POST:\n\t\t\tif (column == null || column.isEmpty()) {\n\t\t\t\tthrow new IllegalArgumentException(\"POST 请求必须在Table内设置要保存的 key:value ！\");\n\t\t\t}\n\n\t\t\tString s = \"\";\n\t\t\tboolean pfirst = true;\n\t\t\tfor (String c : column) {\n\t\t\t\tif (isPrepared() && StringUtil.isName(c) == false) {\n\t\t\t\t\t// 不能通过 ? 来代替，SELECT 'id','name' 返回的就是 id:\"id\", name:\"name\"，而不是数据库里的值！\n\t\t\t\t\tthrow new IllegalArgumentException(\"POST请求: 每一个 key:value 中的key都必须是1个单词！\");\n\t\t\t\t}\n\t\t\t\ts += ((pfirst ? \"\" : \",\") + gainKey(c));\n\n\t\t\t\tpfirst = false;\n\t\t\t}\n\n\t\t\treturn \"(\" + s + \")\";\n\t\tcase GET:\n\t\tcase GETS:\n\t\t\tString joinColumn = \"\";\n\t\t\tif (joinList != null) {\n\t\t\t\tboolean first = true;\n\t\t\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\t\t\tif (join.isAppJoin()) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tSQLConfig<T, M, L> ocfg = join.getOnConfig();\n\t\t\t\t\tboolean isEmpty = ocfg == null || ocfg.getColumn() == null;\n\t\t\t\t\tboolean isLeftOrRightJoin = join.isLeftOrRightJoin();\n\n\t\t\t\t\tif (isEmpty && isLeftOrRightJoin) {\n\t\t\t\t\t\t// 改为 SELECT  ViceTable.* 解决 SELECT sum(ViceTable.id)\n\t\t\t\t\t\t// LEFT/RIGHT JOIN (SELECT sum(id) FROM ViceTable...) AS ViceTable\n\t\t\t\t\t\t// 不仅导致 SQL 函数重复计算，还有时导致 SQL 报错或对应字段未返回\n\t\t\t\t\t\tjoinColumn += (first ? \"\" : \", \") + q + SQLConfig.gainSQLAlias(join.getTable(), join.getAlias()) + q + \".*\";\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tSQLConfig<T, M, L> cfg = isLeftOrRightJoin == false && isEmpty ? join.getJoinConfig() : ocfg;\n\t\t\t\t\t\tif (cfg != null) {\n\t\t\t\t\t\t\tcfg.setMain(false).setKeyPrefix(true);\n\t\t\t\t\t\t\t//if (StringUtil.isEmpty(cfg.getAlias(), true)) {\n\t\t\t\t\t\t\t//\tcfg.setAlias(cfg.getTable());\n\t\t\t\t\t\t\t//}\n\n\t\t\t\t\t\t\tString c = ((AbstractSQLConfig<?, ?, ?>) cfg).gainColumnString(true);\n\t\t\t\t\t\t\tif (StringUtil.isNotEmpty(c, true)) {\n\t\t\t\t\t\t\t\tjoinColumn += (first ? \"\" : \", \") + c;\n\t\t\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tinSQLJoin = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tString tableAlias = q + gainSQLAlias() + q;\n\t\t\t//\tString c = StringUtil.getString(column); //id,name;json_length(contactIdList):contactCount;...\n\n\t\t\tString[] keys = column == null ? null : column.toArray(new String[]{}); //StringUtil.split(c, \";\");\n\t\t\tif (keys == null || keys.length <= 0) {\n\n\t\t\t\tboolean noColumn = column != null && inSQLJoin;\n\t\t\t\tString mc = isKeyPrefix() ? (noColumn ? \"\" : tableAlias + \".*\") : (noColumn ? \"\" : \"*\");\n\n\t\t\t\treturn StringUtil.concat(mc, joinColumn, \", \", true);\n\t\t\t}\n\n\t\t\tList<String> raw = getRaw();\n\t\t\tboolean containRaw = raw != null && raw.contains(KEY_COLUMN);\n\n\t\t\t//...;fun0(arg0,arg1,...):fun0;fun1(arg0,arg1,...):fun1;...\n\t\t\tfor (int i = 0; i < keys.length; i++) {\n\t\t\t\tString expression = keys[i];  //fun(arg0,arg1,...)\n\n\t\t\t\tif (containRaw) {  // 由于 HashMap 对 key 做了 hash 处理，所以 get 比 containsValue 更快\n\t\t\t\t\tif (\"\".equals(RAW_MAP.get(expression)) || RAW_MAP.containsValue(expression)) {  // newSQLConfig<T, M, L> 提前处理好的\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// 简单点， 后台配置就带上 AS\n\t\t\t\t\tint index = expression.lastIndexOf(\":\");\n\t\t\t\t\tString alias = expression.substring(index+1);\n\t\t\t\t\tboolean hasAlias = StringUtil.isName(alias);\n\t\t\t\t\tString pre = index > 0 && hasAlias ? expression.substring(0, index) : expression;\n\t\t\t\t\tif (RAW_MAP.containsValue(pre) || \"\".equals(RAW_MAP.get(pre))) {  // newSQLConfig<T, M, L> 提前处理好的\n\t\t\t\t\t\tkeys[i] = pre + (hasAlias ? gainAs() + q + alias + q : \"\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (expression.length() > 100) {\n\t\t\t\t\tthrow new UnsupportedOperationException(\"@column:value 的 value 中字符串 \" + expression + \" 不合法！\"\n\t\t\t\t\t\t\t+ \"不允许传超过 100 个字符的函数或表达式！请用 @raw 简化传参！\");\n\t\t\t\t}\n\t\t\t\tkeys[i] = parseSQLExpression(KEY_COLUMN, expression, containRaw, true\n\t\t\t\t\t\t, \"@column:\\\"column0,column1:alias1;function0(arg0,arg1,...);function1(...):alias2...\\\"\");\n\t\t\t}\n\n\t\t\tString c = StringUtil.get(keys);\n\t\t\tc = c + (StringUtil.isEmpty(joinColumn, true) ? \"\" : \", \" + joinColumn);//不能在这里改，后续还要用到:\n\t\t\treturn isMain() && isDistinct() ? PREFIX_DISTINCT + c : c;\n\t\tdefault:\n\t\t\tthrow new UnsupportedOperationException(\n\t\t\t\t\t\"服务器内部错误：getColumnString 不支持 \" + RequestMethod.getName(getMethod())\n\t\t\t\t\t+ \" 等 [GET,GETS,HEAD,HEADS,POST] 外的ReuqestMethod！\"\n\t\t\t\t\t);\n\t\t}\n\t}\n\n\t/**解析@column 中以“;”分隔的表达式（\"@column\":\"expression1;expression2;expression2;....\"）中的expression\n\t * @param key\n\t * @param expression\n\t * @param containRaw\n\t * @param allowAlias\n\t * @return\n\t */\n\tpublic String parseSQLExpression(String key, String expression, boolean containRaw, boolean allowAlias) {\n\t\treturn parseSQLExpression(key, expression, containRaw, allowAlias, null);\n\t}\n\t/**解析@column 中以“;”分隔的表达式（\"@column\":\"expression1;expression2;expression2;....\"）中的expression\n\t * @param key\n\t * @param expression\n\t * @param containRaw\n\t * @param allowAlias\n\t * @param example\n\t * @return\n\t */\n\tpublic String parseSQLExpression(String key, String expression, boolean containRaw, boolean allowAlias, String example) {\n\t\tif (containRaw) {\n\t\t\tString s = RAW_MAP.get(expression);\n\t\t\tif (\"\".equals(s)) {\n\t\t\t\treturn expression;\n\t\t\t}\n\t\t\tif (s != null) {\n\t\t\t\treturn s;\n\t\t\t}\n\t\t}\n\n\t\tString quote = getQuote();\n\t\tint start = expression.indexOf('(');\n\t\tif (start < 0) {\n\t\t\t//没有函数 ,可能是字段,也可能是 DISTINCT xx\n\t\t\tString[] cks = parseArgsSplitWithComma(expression, true, containRaw, allowAlias);\n\t\t\texpression = StringUtil.get(cks);\n\t\t} else {  // FIXME 用括号断开？ 如果少的话，用关键词加括号断开，例如  )OVER( 和 )AGAINST(\n\t\t\t// 窗口函数 rank() OVER (PARTITION BY id ORDER BY userId ASC)\n\t\t\t// 全文索引 math(name,tag) AGAINST ('a b +c -d' IN NATURALE LANGUAGE MODE)  // IN BOOLEAN MODE\n\n\t\t\tif (StringUtil.isEmpty(example)) {\n\t\t\t\tif (KEY_COLUMN.equals(key)) {\n\t\t\t\t\texample = key + \":\\\"column0,column1:alias1;function0(arg0,arg1,...);function1(...):alias2...\\\"\";\n\t\t\t\t}\n\t\t\t\t//\t和 key{}:\"\" 一样\t\telse if (KEY_HAVING.equals(key) || KEY_HAVING_AND.equals(key)) {\n\t\t\t\t//\t\t\t\t\texeptionExample = key + \":\\\"function0(arg0,arg1,...)>1;function1(...)%5<=3...\\\"\";\n\t\t\t\t//\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\texample = key + \":\\\"column0!=0;column1+3*2<=10;function0(arg0,arg1,...)>1;function1(...)%5<=3...\\\"\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t//有函数,但不是窗口函数\n\t\t\tint overIndex = expression.indexOf(\")OVER(\");  // 传参不传空格，拼接带空格  \") OVER (\");\n\t\t\tint againstIndex = expression.indexOf(\")AGAINST(\");  // 传参不传空格，拼接带空格  \") AGAINST (\");\n\t\t\tboolean containOver = overIndex > 0 && overIndex < expression.length() - \")OVER(\".length();\n\t\t\tboolean containAgainst = againstIndex > 0 && againstIndex < expression.length() - \")AGAINST(\".length();\n\n\t\t\tif (containOver && containAgainst) {\n\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + expression + \" 不合法！预编译模式下 \" + example\n\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！不能同时存在窗口函数关键词 OVER 和全文索引关键词 AGAINST！\");\n\t\t\t}\n\n\t\t\tif (containOver == false && containAgainst == false) {\n\t\t\t\tint end = expression.lastIndexOf(')');\n\t\t\t\tif (start >= end) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + expression + \" 不合法！\"\n\t\t\t\t\t\t\t+ key + \":value 中 value 里的 SQL函数必须为 function(arg0,arg1,...) 这种格式！\");\n\t\t\t\t}\n\t\t\t\tString fun = expression.substring(0, start);\n\t\t\t\tif (fun.isEmpty() == false) {\n\t\t\t\t\tif (SQL_FUNCTION_MAP == null || SQL_FUNCTION_MAP.isEmpty()) {\n\t\t\t\t\t\tif (StringUtil.isName(fun) == false) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + fun + \" 不合法！预编译模式下 \" + example\n\t\t\t\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (SQL_FUNCTION_MAP.containsKey(fun) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + fun + \" 不合法！预编译模式下 \" + example\n\t\t\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！且必须是后端允许调用的 SQL 函数!\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tString s = expression.substring(start + 1, end);\n\t\t\t\tboolean distinct = s.startsWith(PREFIX_DISTINCT);\n\t\t\t\tif (distinct) {\n\t\t\t\t\ts = s.substring(PREFIX_DISTINCT.length());\n\t\t\t\t}\n\n\t\t\t\t// 解析函数内的参数\n\t\t\t\tString ckeys[] = parseArgsSplitWithComma(s, false, containRaw, allowAlias);\n\n\t\t\t\tString suffix = expression.substring(end + 1); //:contactCount\n\t\t\t\tString alias = null;\n\t\t\t\tif (allowAlias) {\n\t\t\t\t\tint index = suffix.lastIndexOf(\":\");\n\t\t\t\t\talias = index < 0 ? \"\" : suffix.substring(index + 1); //contactCount\n\t\t\t\t\tsuffix = index < 0 ? suffix : suffix.substring(0, index);\n\t\t\t\t\tif (alias.isEmpty() == false && StringUtil.isName(alias) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符串 \" + alias + \" 不合法！预编译模式下 \"\n\t\t\t\t\t\t\t\t+ key + \":value 中 value里面用 ; 分割的每一项\"\n\t\t\t\t\t\t\t\t+ \" function(arg0,arg1,...):alias 中 alias 必须是1个单词！并且不要有多余的空格！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (suffix.isEmpty() == false && (suffix.contains(\"--\") || suffix.contains(\"/*\")\n\t\t\t\t\t\t|| PATTERN_RANGE.matcher(suffix).matches() == false)) {\n\t\t\t\t\tthrow new UnsupportedOperationException(\"字符串 \" + suffix + \" 不合法！预编译模式下 \" + key\n\t\t\t\t\t\t\t+ \":\\\"column?value;function(arg0,arg1,...)?value...\\\"\"\n\t\t\t\t\t\t\t+ \" 中 ?value 必须符合正则表达式 \" + PATTERN_RANGE + \" 且不包含连续减号 -- 或注释符 /* ！不允许多余的空格！\");\n\t\t\t\t}\n\n\t\t\t\tString origin = fun + \"(\" + (distinct ? PREFIX_DISTINCT : \"\") + StringUtil.get(ckeys) + \")\" + suffix;\n\t\t\t\texpression = origin + (StringUtil.isEmpty(alias, true) ? \"\" : gainAs() + quote + alias + quote);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t//是窗口函数   fun(arg0,agr1) OVER (agr0 agr1 ...)\n\t\t\t\tint keyIndex = containOver ? overIndex : againstIndex;\n\t\t\t\tString s1 = expression.substring(0, keyIndex + 1); // OVER 前半部分\n\t\t\t\tString s2 = expression.substring(keyIndex + 1); // OVER 后半部分\n\n\t\t\t\tint index1 = s1.indexOf(\"(\"); //  函数 \"(\" 的起始位置\n\t\t\t\tint end = s2.lastIndexOf(\")\"); // 后半部分 “)” 的位置\n\n\t\t\t\tif (index1 >= end + s1.length()) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + expression + \" 不合法！\"\n\t\t\t\t\t\t\t+ key + \":value 中 value 里的 SQL 函数必须为 function(arg0,arg1,...) 这种格式！\");\n\t\t\t\t}\n\n\t\t\t\tString fun = s1.substring(0, index1); // 函数名称\n\t\t\t\tif (fun.isEmpty() == false) {\n\t\t\t\t\tif (SQL_FUNCTION_MAP == null || SQL_FUNCTION_MAP.isEmpty()) {\n\t\t\t\t\t\tif (StringUtil.isName(fun) == false) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + fun + \" 不合法！预编译模式下 \" + example\n\t\t\t\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (SQL_FUNCTION_MAP.containsKey(fun) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + fun + \" 不合法！预编译模式下 \" + example\n\t\t\t\t\t\t\t\t+ \" 中 function 必须符合小写英文单词的 SQL 函数名格式！且必须是后端允许调用的 SQL 函数!\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 获取前半部分函数的参数解析   fun(arg0,agr1)\n\t\t\t\tString agrsString1[] = parseArgsSplitWithComma(\n\t\t\t\t\t\ts1.substring(index1 + 1, s1.lastIndexOf(\")\")), false, containRaw, allowAlias\n\t\t\t\t);\n\n\t\t\t\tint index2 = s2.indexOf(\"(\"); // 后半部分 “(”的起始位置\n\t\t\t\tString argString2 = s2.substring(index2 + 1, end); // 后半部分的参数\n\t\t\t\t// 别名\n\t\t\t\tint aliasIndex = allowAlias ? s2.lastIndexOf(\":\") : -1;\n\t\t\t\tString alias = aliasIndex < 0 ? \"\" : s2.substring(aliasIndex + 1);\n\t\t\t\tif (alias.isEmpty() == false && StringUtil.isName(alias) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"字符串 \" + alias + \" 不合法！预编译模式下 \"\n\t\t\t\t\t\t\t+ key + \":value 中 value里面用 ; 分割的每一项\"\n\t\t\t\t\t\t\t+ \" function(arg0,arg1,...):alias 中 alias 必须是1个单词！并且不要有多余的空格！\");\n\t\t\t\t}\n\n\t\t\t\tString suffix = s2.substring(end + 1, aliasIndex < 0 ? s2.length() : aliasIndex);\n\t\t\t\tif (suffix.isEmpty() == false && (suffix.contains(\"--\") || suffix.contains(\"/*\")\n\t\t\t\t\t\t|| PATTERN_RANGE.matcher(suffix).matches() == false)) {\n\t\t\t\t\tthrow new UnsupportedOperationException(\"字符串 \" + suffix + \" 不合法！预编译模式下 \" + key\n\t\t\t\t\t\t\t+ \":\\\"column?value;function(arg0,arg1,...)?value...\\\"\"\n\t\t\t\t\t\t\t+ \" 中 ?value 必须符合正则表达式 \" + PATTERN_RANGE + \" 且不包含连续减号 -- 或注释符 /* ！不允许多余的空格！\");\n\t\t\t\t}\n\n\t\t\t\t// 获取后半部分的参数解析 (agr0 agr1 ...)\n\t\t\t\tString[] argsString2 = parseArgsSplitWithComma(argString2, false, containRaw, allowAlias);\n\t\t\t\texpression = fun + \"(\" + StringUtil.get(agrsString1) + (containOver ? \") OVER (\" : \") AGAINST (\")\n\t\t\t\t\t\t+ StringUtil.get(argsString2) + \")\" + suffix  // 传参不传空格，拼接带空格\n\t\t\t\t\t\t+ (StringUtil.isEmpty(alias, true) ? \"\" : gainAs() + quote + alias + quote);\n\t\t\t}\n\t\t}\n\n\t\treturn expression;\n\t}\n\n\n\t/**解析函数参数或者字段,此函数对于解析字段 和 函数内参数通用\n\t * @param param\n\t * @param isColumn true:不是函数参数。false:是函数参数\n\t * @param containRaw\n\t * @param allowAlias\n\t * @return\n\t */\n\tprivate String[] parseArgsSplitWithComma(String param, boolean isColumn, boolean containRaw, boolean allowAlias) {\n\t\t// 以\",\" 分割参数\n\t\tString quote = getQuote();\n\t\tboolean isKeyPrefix = isKeyPrefix();\n\t\tString tableAlias = quote + gainSQLAlias() + quote;\n\t\tString[] ckeys = StringUtil.split(param); // 以\",\"分割参数\n\t\tif (ckeys != null && ckeys.length > 0) {\n\n\t\t\tfor (int i = 0; i < ckeys.length; i++) {\n\t\t\t\tString ck = ckeys[i];\n\n\t\t\t\tString origin;\n\t\t\t\tString alias;\n\n\t\t\t\t// 如果参数包含 \"'\" ,解析字符串\n\t\t\t\tif (ck.startsWith(\"`\") && ck.endsWith(\"`\")) {\n\t\t\t\t\torigin = ck.substring(1, ck.length() - 1);\n\t\t\t\t\t//sql 注入判断 判断\n\t\t\t\t\tif (origin.startsWith(\"_\") || StringUtil.isName(origin) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"`column0`,`column1`:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t\t+ \" 中所有字符串 column 都必须必须为1个单词 ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\torigin = gainKey(origin);\n\t\t\t\t}\n\t\t\t\telse if (ck.startsWith(\"'\") && ck.endsWith(\"'\")) {\n\t\t\t\t\torigin = ck.substring(1, ck.length() - 1);\n\t\t\t\t\tif (origin.contains(\"'\")) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符串 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"column0,column1:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t\t+ \" 中字符串参数不合法，必须以 ' 开头, ' 结尾,字符串中不能包含 ' \");\n\t\t\t\t\t}\n\n\t\t\t\t\t// 1.字符串不是字段也没有别名,所以不解析别名 2. 是字符串，进行预编译，使用getValue() ,对字符串进行截取\n\t\t\t\t\torigin = gainValue(origin).toString();\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\t// 参数不包含\",\",即不是字符串\n\t\t\t\t\t// 解析参数:1. 字段 ,2. 是以空格分隔的参数 eg: cast(now() as date)\n\t\t\t\t\tif (\"=null\".equals(ck)) {\n\t\t\t\t\t\torigin = SQL.isNull();\n\t\t\t\t\t}\n\t\t\t\t\telse if (\"!=null\".equals(ck)) {\n\t\t\t\t\t\torigin = SQL.isNull(false);\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\torigin = ck;\n\t\t\t\t\t\talias = null;\n\t\t\t\t\t\tif (allowAlias) {\n\t\t\t\t\t\t\tint index = isColumn ? ck.lastIndexOf(\":\") : -1; //StringUtil.split返回数组中，子项不会有null\n\t\t\t\t\t\t\torigin = index < 0 ? ck : ck.substring(0, index); //获取 : 之前的\n\t\t\t\t\t\t\talias = index < 0 ? null : ck.substring(index + 1);\n\t\t\t\t\t\t\tif (isPrepared()) {\n\t\t\t\t\t\t\t\tif (isColumn) {\n\t\t\t\t\t\t\t\t\tif (StringUtil.isName(origin) == false || (alias != null && StringUtil.isName(alias) == false)) {\n\t\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:value 中 value里面用 , 分割的每一项\"\n\t\t\t\t\t\t\t\t\t\t\t\t+ \" column:alias 中 column 必须是1个单词！如果有alias，则alias也必须为1个单词！\"\n\t\t\t\t\t\t\t\t\t\t\t\t+ \"关键字必须全大写，且以空格分隔的参数，空格必须只有 1 个！其它情况不允许空格！\");\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tif (origin.startsWith(\"_\") || origin.contains(\"--\")) {\n\t\t\t\t\t\t\t\t\t\t// || PATTERN_FUNCTION.matcher(origin).matches() == false) {\n\t\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"column0,column1:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t\t\t\t\t\t+ \" 中所有 arg 都必须是1个不以 _ 开头的单词 或者符合正则表达式 \"\n\t\t\t\t\t\t\t\t\t\t\t\t+ PATTERN_FUNCTION + \" 且不包含连续减号 -- ！\" +\n\t\t\t\t\t\t\t\t\t\t\t\t\"DISTINCT 必须全大写，且后面必须有且只有 1 个空格！其它情况不允许空格！\");\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// 以空格分割参数\n\t\t\t\t\t\tString[] mkes = containRaw ? StringUtil.split(ck, \" \", true) : new String[]{ ck };\n\n\t\t\t\t\t\t//如果参数中含有空格(少数情况) 比如  fun(arg1, arg2,arg3,arg4) 中的 arg1 arg2 arg3，比如 DISTINCT id\n\t\t\t\t\t\tif (mkes != null && mkes.length >= 2) {\n\t\t\t\t\t\t\torigin = parseArgsSplitWithSpace(mkes);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tString mk = RAW_MAP.get(origin);\n\t\t\t\t\t\t\tif (mk != null) {  // newSQLConfig<T, M, L> 提前处理好的\n\t\t\t\t\t\t\t\tif (mk.length() > 0) {\n\t\t\t\t\t\t\t\t\torigin = mk;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (StringUtil.isNumber(origin)) {\n\t\t\t\t\t\t\t\t//do nothing\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tString[] keys = origin.split(\"[.]\");\n\t\t\t\t\t\t\t\tStringBuilder sb = new StringBuilder();\n\n\t\t\t\t\t\t\t\tint len = keys == null ? 0 : keys.length;\n\t\t\t\t\t\t\t\tif (len > 0) {\n\t\t\t\t\t\t\t\t\tboolean first = true;\n\t\t\t\t\t\t\t\t\tfor (String k : keys) {\n\t\t\t\t\t\t\t\t\t\tif (StringUtil.isName(k) == false) {\n\t\t\t\t\t\t\t\t\t\t\tsb = null;\n\t\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t\tsb.append(first ? \"\" : \".\").append(quote).append(k).append(quote);\n\t\t\t\t\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tString s = sb == null ? null : sb.toString();\n\t\t\t\t\t\t\t\tif (StringUtil.isNotEmpty(s, true)) {\n\t\t\t\t\t\t\t\t\torigin = (len == 1 && isKeyPrefix ? tableAlias + \".\" : \"\") + s;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\torigin = gainValue(origin).toString();\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (isColumn && StringUtil.isNotEmpty(alias, true)) {\n\t\t\t\t\t\t\t\torigin += gainAs() + quote + alias + quote;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tckeys[i] = origin;\n\t\t\t}\n\t\t}\n\n\t\treturn ckeys;\n\t}\n\n\n\t/**\n\t * 只解析以空格分隔的参数\n\t *\n\t * @param mkes\n\t * @return\n\t */\n\tprivate String parseArgsSplitWithSpace(String[] mkes) {\n\t\tString quote = getQuote();\n\t\tboolean isKeyPrefix = isKeyPrefix();\n\t\tString tableAlias = quote + gainSQLAlias() + quote;\n\n\t\t// 包含空格的参数  肯定不包含别名 不用处理别名\n\t\tif (mkes != null && mkes.length > 0) {\n\t\t\tfor (int j = 0; j < mkes.length; j++) {\n\t\t\t\t// now()/AS/ DISTINCT/VALUE 等等放在RAW_MAP中\n\t\t\t\tString origin = mkes[j];\n\n\t\t\t\tString mk = RAW_MAP.get(origin);\n\t\t\t\tif (mk != null) {  // newSQLConfig<T, M, L> 提前处理好的\n\t\t\t\t\tif (mk.length() > 0) {\n\t\t\t\t\t\tmkes[j] = mk;\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t//这里为什么还要做一次判断 是因为解析窗口函数调用的时候会判断一次\n\t\t\t\tString ck = origin;\n\t\t\t\t// 如果参数包含 \"`\" 或 \"'\" ,解析字符串\n\t\t\t\tif (ck.startsWith(\"`\") && ck.endsWith(\"`\")) {\n\t\t\t\t\torigin = ck.substring(1, ck.length() - 1);\n\t\t\t\t\tif (origin.startsWith(\"_\") || StringUtil.isName(origin) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"`column0`,`column1`:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t\t+ \" 中所有字符串 column 都必须必须为1个单词 ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tmkes[j] = gainKey(origin);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\telse if (ck.startsWith(\"'\") && ck.endsWith(\"'\")) {\n\t\t\t\t\torigin = ck.substring(1, ck.length() - 1);\n\t\t\t\t\tif (origin.contains(\"'\")) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"字符串 \" + ck + \" 不合法！\"\n\t\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"column0,column1:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t\t+ \" 中字符串参数不合法，必须以 ' 开头, ' 结尾,字符串中不能包含 ' \");\n\t\t\t\t\t}\n\n\t\t\t\t\t// 1.字符串不是字段也没有别名,所以不解析别名 2. 是字符串，进行预编译，使用getValue() ,对字符串进行截取\n\t\t\t\t\tmkes[j] = gainValue(origin).toString();\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\telse if (ck.contains(\"`\") || ck.contains(\"'\") || origin.startsWith(\"_\") || origin.contains(\"--\")) {\n\t\t\t\t\t// || PATTERN_FUNCTION.matcher(origin).matches() == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"字符 \" + origin + \" 不合法！\"\n\t\t\t\t\t\t\t+ \"预编译模式下 @column:\\\"column0,column1:alias;function0(arg0,arg1,...);function1(...):alias...\\\"\"\n\t\t\t\t\t\t\t+ \" 中所有 arg 都必须是1个不以 _ 开头的单词 或者符合正则表达式 \" + PATTERN_FUNCTION\n\t\t\t\t\t\t\t+ \" 且不包含连续减号 -- ！DISTINCT 必须全大写，且后面必须有且只有 1 个空格！其它情况不允许空格！\");\n\t\t\t\t}\n\n\t\t\t\tif (StringUtil.isNumber(origin)) {\n\t\t\t\t\t//do nothing\n\t\t\t\t} else {\n\t\t\t\t\tString[] keys = origin.split(\"[.]\");\n\t\t\t\t\tStringBuilder sb = new StringBuilder();\n\n\t\t\t\t\tint len = keys == null ? 0 : keys.length;\n\t\t\t\t\tif (len > 0) {\n\t\t\t\t\t\tboolean first = true;\n\t\t\t\t\t\tfor (String k : keys) {\n\t\t\t\t\t\t\tif (StringUtil.isName(k) == false) {\n\t\t\t\t\t\t\t\tsb = null;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tsb.append(first ? \"\" : \".\").append(quote).append(k).append(quote);\n\t\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tString s = sb == null ? null : sb.toString();\n\t\t\t\t\tif (StringUtil.isNotEmpty(s, true)) {\n\t\t\t\t\t\torigin = (len == 1 && isKeyPrefix ? tableAlias + \".\" : \"\") + s;\n\t\t\t\t\t} else {\n\t\t\t\t\t\torigin = gainValue(origin).toString();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tmkes[j] = origin;\n\t\t\t}\n\t\t}\n\t\t// 返回重新以\" \"拼接后的参数\n\t\treturn StringUtil.join(mkes, \" \");\n\t}\n\n\n\t@Override\n\tpublic List<List<Object>> getValues() {\n\t\treturn values;\n\t}\n\tpublic String getValuesString() {\n\t\tString s = \"\";\n\t\tif (values != null && values.size() > 0) {\n\t\t\tObject[] items = new Object[values.size()];\n\t\t\tList<Object> vs;\n\t\t\tfor (int i = 0; i < values.size(); i++) {\n\t\t\t\tvs = values.get(i);\n\t\t\t\tif (vs == null) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\titems[i] = \"(\";\n\t\t\t\tfor (int j = 0; j < vs.size(); j++) {\n\t\t\t\t\titems[i] += ((j <= 0 ? \"\" : \",\") + gainValue(vs.get(j)));\n\t\t\t\t}\n\t\t\t\titems[i] += \")\";\n\t\t\t}\n\t\t\ts = StringUtil.get(items);\n\t\t}\n\t\treturn s;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setValues(List<List<Object>> valuess) {\n\t\tthis.values = valuess;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Map<String, Object> getContent() {\n\t\treturn content;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setContent(Map<String, Object> content) {\n\t\tthis.content = content;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic int getCount() {\n\t\treturn count;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCount(int count) {\n\t\tthis.count = count;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic int getPage() {\n\t\treturn page;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setPage(int page) {\n\t\tthis.page = page;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic int getPosition() {\n\t\treturn position;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setPosition(int position) {\n\t\tthis.position = position;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic int getQuery() {\n\t\treturn query;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setQuery(int query) {\n\t\tthis.query = query;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic Boolean getCompat() {\n\t\treturn compat;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCompat(Boolean compat) {\n\t\tthis.compat = compat;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic int getType() {\n\t\treturn type;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setType(int type) {\n\t\tthis.type = type;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic int getCache() {\n\t\treturn cache;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCache(int cache) {\n\t\tthis.cache = cache;\n\t\treturn this;\n\t}\n\n\tpublic AbstractSQLConfig<T, M, L> setCache(String cache) {\n\t\treturn setCache(getCache(cache));\n\t}\n\tpublic static int getCache(String cache) {\n\t\tint cache2;\n\t\tif (cache == null) {\n\t\t\tcache2 = JSONMap.CACHE_ALL;\n\t\t}\n\t\telse {\n\t\t\t//\t\t\tif (isSubquery) {\n\t\t\t//\t\t\t\tthrow new IllegalArgumentException(\"子查询内不支持传 \" + apijson.JSONMap.KEY_CACHE + \"!\");\n\t\t\t//\t\t\t}\n\n\t\t\tswitch (cache) {\n\t\t\tcase \"0\":\n\t\t\tcase JSONMap.CACHE_ALL_STRING:\n\t\t\t\tcache2 = JSONMap.CACHE_ALL;\n\t\t\t\tbreak;\n\t\t\tcase \"1\":\n\t\t\tcase JSONMap.CACHE_ROM_STRING:\n\t\t\t\tcache2 = JSONMap.CACHE_ROM;\n\t\t\t\tbreak;\n\t\t\tcase \"2\":\n\t\t\tcase JSONMap.CACHE_RAM_STRING:\n\t\t\t\tcache2 = JSONMap.CACHE_RAM;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tthrow new IllegalArgumentException(JSONMap.KEY_CACHE\n\t\t\t\t\t\t+ \":value 中 value 的值不合法！必须在 [0,1,2] 或 [ALL, ROM, RAM] 内 !\");\n\t\t\t}\n\t\t}\n\t\treturn cache2;\n\t}\n\n\t@Override\n\tpublic boolean isExplain() {\n\t\treturn explain;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setExplain(boolean explain) {\n\t\tthis.explain = explain;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic List<Join<T, M, L>> getJoinList() {\n\t\treturn joinList;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setJoinList(List<Join<T, M, L>> joinList) {\n\t\tthis.joinList = joinList;\n\t\treturn this;\n\t}\n\t@Override\n\tpublic boolean hasJoin() {\n\t\treturn joinList != null && joinList.isEmpty() == false;\n\t}\n\n\n\t@Override\n\tpublic boolean isTest() {\n\t\treturn test;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setTest(boolean test) {\n\t\tthis.test = test;\n\t\treturn this;\n\t}\n\n\t/**获取初始位置offset\n\t * @return\n\t */\n\tpublic int getOffset() {\n\t\treturn getOffset(getPage(), getCount());\n\t}\n\t/**获取初始位置offset\n\t * @param page\n\t * @param count\n\t * @return\n\t */\n\tpublic static int getOffset(int page, int count) {\n\t\treturn page*count;\n\t}\n\t/**获取限制数量\n\t * @return\n\t */\n\tpublic String gainLimitString() {\n\t\tint count = getCount();\n\t\tint page = getPage();\n\n\t\tboolean isMilvus = isMilvus();\n\t\tif ((count <= 0 && ! (isMilvus && isMain())) || RequestMethod.isHeadMethod(getMethod(), true)) { // TODO HEAD 真的不需要 LIMIT ？\n\t\t\treturn \"\";\n\t\t}\n\n\t\tboolean isSurrealDB = isSurrealDB();\n\t\tboolean isQuestDB = isQuestDB();\n\t\tif (isSurrealDB || isQuestDB || isMilvus) {\n\t\t\tif (count == 0) {\n\t\t\t\tParser<T, M, L> parser = gainParser();\n\t\t\t\tcount = parser == null ? AbstractParser.MAX_QUERY_COUNT : parser.getMaxQueryCount();\n\t\t\t}\n\n\t\t\tint offset = getOffset(page, count);\n\t\t\tif (isQuestDB()) {\n\t\t\t\treturn \" LIMIT \" + offset + \", \" + (offset + count);\n\t\t\t}\n\t\t\telse if (isSurrealDB()) {\n\t\t\t\treturn \" START \" + offset + \" LIMIT \" + count;\n\t\t\t}\n\t\t\telse {\n\t\t\t\treturn \" LIMIT \" + offset + \", \" + count; // 目前 moql-transx 的限制\n\t\t\t}\n\t\t}\n\n\t\tboolean isOracle = isOracle();\n\t\treturn gainLimitString(page, count, isTSQL(), isOracle || isDameng() || isKingBase(), isPresto() || isTrino());\n\t}\n\t/**获取限制数量及偏移量\n\t* @param page\n\t* @param count\n\t* @param isTSQL\n\t* @param isOracle\n\t* @return\n\t*/\n\tpublic static String gainLimitString(int page, int count, boolean isTSQL, boolean isOracle) {\n\t\treturn gainLimitString(page, count, isTSQL, isOracle, false);\n\t}\n\t/**获取限制数量及偏移量\n\t* @param page\n\t* @param count\n\t* @param isTSQL\n\t* @param isOracle\n\t* @param isPresto\n\t* @return\n\t*/\n\tpublic static String gainLimitString(int page, int count, boolean isTSQL, boolean isOracle, boolean isPresto) {\n\t\tint offset = getOffset(page, count);\n\n\t\tif (isOracle) {  // TODO 判断版本，高版本可以用 OFFSET FETCH\n\t\t\treturn \" WHERE ROWNUM BETWEEN \" + offset + \" AND \" + (offset + count);\n\t\t}\n\n\t\tif (isTSQL) {  // OFFSET FECTH 中所有关键词都不可省略, 另外 Oracle 数据库使用子查询加 where 分页\n\t\t\treturn \" OFFSET \" + offset + \" ROWS FETCH FIRST \" + count + \" ROWS ONLY\";\n\t\t}\n\n\t\tif (isPresto) {  // https://prestodb.io/docs/current/sql/select.html\n\t\t\treturn (offset <= 0 ? \"\" : \" OFFSET \" + offset) + \" LIMIT \" + count;\n\t\t}\n\n\t\treturn \" LIMIT \" + count + (offset <= 0 ? \"\" : \" OFFSET \" + offset);  // DELETE, UPDATE 不支持 OFFSET\n\t}\n\n\t@Override\n\tpublic List<String> getNull() {\n\t\treturn nulls;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setNull(List<String> nulls) {\n\t\tthis.nulls = nulls;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Map<String, String> getCast() {\n\t\treturn cast;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCast(Map<String, String> cast) {\n\t\tthis.cast = cast;\n\t\treturn this;\n\t}\n\n\t//WHERE <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tprotected int getMaxHavingCount() {\n\t\treturn MAX_HAVING_COUNT;\n\t}\n\tprotected int getMaxWhereCount() {\n\t\treturn MAX_WHERE_COUNT;\n\t}\n\tprotected int getMaxCombineDepth() {\n\t\treturn MAX_COMBINE_DEPTH;\n\t}\n\tprotected int getMaxCombineCount() {\n\t\treturn MAX_COMBINE_COUNT;\n\t}\n\tprotected int getMaxCombineKeyCount() {\n\t\treturn MAX_COMBINE_KEY_COUNT;\n\t}\n\tprotected float getMaxCombineRatio() {\n\t\treturn MAX_COMBINE_RATIO;\n\t}\n\n\n\t@Override\n\tpublic String getCombine() {\n\t\treturn combine;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCombine(String combine) {\n\t\tthis.combine = combine;\n\t\treturn this;\n\t}\n\n\t@NotNull\n\t@Override\n\tpublic Map<String, List<String>> getCombineMap() {\n\t\tList<String> andList = combineMap == null ? null : combineMap.get(\"&\");\n\t\tif (andList == null) {\n\t\t\tandList = where == null ? new ArrayList<String>() : new ArrayList<String>(where.keySet());\n\t\t\tif (combineMap == null) {\n\t\t\t\tcombineMap = new HashMap<>();\n\t\t\t}\n\t\t\tcombineMap.put(\"&\", andList);\n\t\t}\n\t\treturn combineMap;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setCombineMap(Map<String, List<String>> combineMap) {\n\t\tthis.combineMap = combineMap;\n\t\treturn this;\n\t}\n\n\t@Override\n\tpublic Map<String, Object> getWhere() {\n\t\treturn where;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setWhere(Map<String, Object> where) {\n\t\tthis.where = where;\n\t\treturn this;\n\t}\n\n\t/**\n\t * noFunctionChar = false\n\t * @param key\n\t * @return\n\t */\n\t@Override\n\tpublic Object getWhere(String key) {\n\t\treturn getWhere(key, false);\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48\n\t/**\n\t * @param key - the key passed in\n\t * @param exactMatch - whether it is exact match\n\t * @return\n\t * <p>use entrySet+getValue() to replace keySet+get() to enhance efficiency</p>\n\t */\n\t@Override\n\tpublic Object getWhere(String key, boolean exactMatch) {\n\t\tif (exactMatch) {\n\t\t\treturn where == null ? null : where.get(key);\n\t\t}\n\n\t\tif (key == null || where == null){\n\t\t\treturn null;\n\t\t}\n\n\t\tint index;\n\t\tfor (Entry<String,Object> entry : where.entrySet()) {\n\t\t\tString k = entry.getKey();\n\t\t\tindex = k.indexOf(key);\n\t\t\tif (index >= 0 && StringUtil.isName(k.substring(index, index + 1)) == false) {\n\t\t\t\treturn entry.getValue();\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> putWhere(String key, Object value, boolean prior) {\n\t\tif (key != null) {\n\t\t\tif (where == null) {\n\t\t\t\twhere = new LinkedHashMap<>();\n\t\t\t}\n\t\t\tif (value == null) {\n\t\t\t\twhere.remove(key);\n\t\t\t} else {\n\t\t\t\twhere.put(key, value);\n\t\t\t}\n\n\t\t\tMap<String, List<String>> combineMap = getCombineMap();\n\t\t\tList<String> andList = combineMap.get(\"&\");\n\t\t\tif (value == null) {\n\t\t\t\tif (andList != null) {\n\t\t\t\t\tandList.remove(key);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (andList == null || andList.contains(key) == false) {\n\t\t\t\tint i = 0;\n\t\t\t\tif (andList == null) {\n\t\t\t\t\tandList = new ArrayList<>();\n\t\t\t\t}\n\t\t\t\telse if (prior && andList.isEmpty() == false) {\n\n\t\t\t\t\tString idKey = getIdKey();\n\t\t\t\t\tString idInKey = idKey + \"{}\";\n\t\t\t\t\tString userIdKey = getUserIdKey();\n\t\t\t\t\tString userIdInKey = userIdKey + \"{}\";\n\n\t\t\t\t\tint lastIndex;\n\t\t\t\t\tif (key.equals(idKey)) {\n\t\t\t\t\t\tsetId(value);\n\t\t\t\t\t\tlastIndex = -1;\n\t\t\t\t\t}\n\t\t\t\t\telse if (key.equals(idInKey)) {\n\t\t\t\t\t\tsetIdIn(value);\n\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idKey);\n\t\t\t\t\t}\n\t\t\t\t\telse if (key.equals(userIdKey)) {\n\t\t\t\t\t\tsetUserId(value);\n\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idInKey);\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (key.equals(userIdInKey)) {\n\t\t\t\t\t\tsetUserIdIn(value);\n\t\t\t\t\t\tlastIndex = andList.lastIndexOf(userIdKey);\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idInKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tlastIndex = andList.lastIndexOf(userIdInKey);\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(userIdKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idInKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (lastIndex < 0) {\n\t\t\t\t\t\t\tlastIndex = andList.lastIndexOf(idKey);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\ti = lastIndex + 1;\n\t\t\t\t}\n\n\t\t\t\tif (prior) {\n\t\t\t\t\tandList.add(i, key); // userId 的优先级不能比 id 高  0, key);\n\t\t\t\t} else {\n\t\t\t\t\t// AbstractSQLExecutor.onPutColumn 里 getSQL，要保证缓存的 SQL 和查询的 SQL 里 where 的 key:value 顺序一致\n\t\t\t\t\tandList.add(key);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcombineMap.put(\"&\", andList);\n\t\t}\n\n\t\treturn this;\n\t}\n\n\t/**获取WHERE\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic String gainWhereString(boolean hasPrefix) throws Exception {\n\t\tString combineExpr = getCombine();\n\t\tif (StringUtil.isEmpty(combineExpr, false)) {\n\t\t\treturn getWhereString(hasPrefix, getMethod(), getWhere(), getCombineMap(), getJoinList(), ! isTest());\n\t\t}\n\t\treturn getWhereString(hasPrefix, getMethod(), getWhere(), combineExpr, getJoinList(), ! isTest());\n\t}\n\t/**获取WHERE\n\t * @param method\n\t * @param where\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic String getWhereString(boolean hasPrefix, RequestMethod method, Map<String, Object> where\n\t\t\t, String combine, List<Join<T, M, L>> joinList, boolean verifyName) throws Exception {\n\t\tString whereString = parseCombineExpression(method, getQuote(), getTable(), getAlias()\n\t\t\t\t, where, combine, verifyName, false, false);\n\t\twhereString = concatJoinWhereString(whereString);\n\t\tString result = StringUtil.isEmpty(whereString, true) ? \"\" : (hasPrefix ? \" WHERE \" : \"\") + whereString;\n\n\t\tif (result.isEmpty() && RequestMethod.isQueryMethod(method) == false) {\n\t\t\tthrow new UnsupportedOperationException(\"写操作请求必须带条件！！！\");\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**解析 @combine 条件 key 组合的与或非+括号的逻辑运算表达式为具体的完整条件组合\n\t * @param method\n\t * @param quote\n\t * @param table\n\t * @param alias\n\t * @param conditionMap  where 或 having 对应条件的 Map\n\t * @param combine\n\t * @param verifyName\n\t * @param containRaw\n\t * @param isHaving\n\t * @return\n\t * @throws Exception\n\t */\n\tprotected String parseCombineExpression(RequestMethod method, String quote, String table, String alias\n\t\t\t, Map<String, Object> conditionMap, String combine, boolean verifyName, boolean containRaw, boolean isHaving) throws Exception {\n\n\t\tString errPrefix = table + (isHaving ? \":{ @having:{ \" : \":{ \") + \"@combine:'\" + combine + (isHaving ? \"' } }\" : \"' }\");\n\t\tString s = StringUtil.get(combine);\n\t\tif (s.startsWith(\" \") || s.endsWith(\" \") ) {\n\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s\n\t\t\t\t\t+ \"' 不合法！不允许首尾有空格，也不允许连续空格！空格不能多也不能少！\"\n\t\t\t\t\t+ \"逻辑连接符 & | 左右必须各一个相邻空格！左括号 ( 右边和右括号 ) 左边都不允许有相邻空格！\");\n\t\t}\n\n\t\tif (conditionMap == null) {\n\t\t\tconditionMap = new HashMap<>();\n\t\t}\n\t\tint size = conditionMap.size();\n\n\t\tint maxCount = isHaving ? getMaxHavingCount() : getMaxWhereCount();\n\t\tif (maxCount > 0 && size > maxCount) {\n\t\t\tthrow new IllegalArgumentException(table + (isHaving ? \":{ @having:{ \" : \":{ \") + \"key0:value0, key1:value1... \" + combine\n\t\t\t\t\t+ (isHaving ? \" } }\" : \" }\") + \" 中条件 key:value 数量 \" + size + \" 已超过最大数量，必须在 0-\" + maxCount + \" 内！\");\n\t\t}\n\n\t\tString result = \"\";\n\n\t\tList<Object> preparedValues = getPreparedValueList();\n\t\tif (preparedValues == null && isHaving == false) {\n\t\t\tpreparedValues = new ArrayList<>();\n\t\t}\n\n\t\tMap<String, Integer> usedKeyCountMap = new HashMap<>(size);\n\n\t\tint n = s.length();\n\t\tif (n > 0) {\n\t\t\tif (isHaving == false) {  // 只收集表达式条件值\n\t\t\t\tsetPreparedValueList(new ArrayList<>());  // 必须反过来，否则 JOIN ON 内部 @combine 拼接后顺序错误\n\t\t\t}\n\n\t\t\tint maxDepth = getMaxCombineDepth();\n\t\t\tint maxCombineCount = getMaxCombineCount();\n\t\t\tint maxCombineKeyCount = getMaxCombineKeyCount();\n\t\t\tfloat maxCombineRatio = getMaxCombineRatio();\n\n\t\t\tint depth = 0;\n\t\t\tint allCount = 0;\n\n\t\t\tint i = 0;\n\n\t\t\tchar lastLogic = 0;\n\t\t\tchar last = 0;\n\t\t\tboolean first = true;\n\t\t\tboolean isNot = false;\n\n\t\t\tString key = \"\";\n\t\t\twhile (i <= n) {  // \"date> | (contactIdList<> & (name*~ | tag&$))\"\n\t\t\t\tboolean isOver = i >= n;\n\t\t\t\tchar c = isOver ? 0 : s.charAt(i);\n\t\t\t\tboolean isBlankOrRightParenthesis = c == ' ' || c == ')';\n\t\t\t\tif (isOver || isBlankOrRightParenthesis) {\n\t\t\t\t\tboolean isEmpty = StringUtil.isEmpty(key, true);\n\t\t\t\t\tif (isEmpty && last != ')') {\n\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + (isOver ? s : s.substring(i))\n\t\t\t\t\t\t\t\t+ \"' 不合法！\" + (c == ' ' ? \"空格 ' ' \" : \"右括号 ')'\") + \" 左边缺少条件 key ！逻辑连接符 & | 左右必须各一个相邻空格！\"\n\t\t\t\t\t\t\t\t+ \"空格不能多也不能少！不允许首尾有空格，也不允许连续空格！左括号 ( 的右边 和 右括号 ) 的左边 都不允许有相邻空格！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (isEmpty == false) {\n\t\t\t\t\t\tif (first == false && lastLogic <= 0) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 \"\n\t\t\t\t\t\t\t\t\t+ \"'\" + s.substring(i - key.length() - (isOver ? 1 : 0))\n\t\t\t\t\t\t\t\t\t+ \"' 不合法！左边缺少 & | 其中一个逻辑连接符！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tallCount ++;\n\t\t\t\t\t\tif (allCount > maxCombineCount && maxCombineCount > 0) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s + \"' 不合法！\"\n\t\t\t\t\t\t\t\t\t+ \"其中 key 数量 \" + allCount + \" 已超过最大值，必须在条件键值对数量 0-\" + maxCombineCount + \" 内！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tString column = key;\n\t\t\t\t\t\tint keyIndex = column.indexOf(\":\");\n\t\t\t\t\t\tcolumn = keyIndex > 0 ? column.substring(0, keyIndex) : column;\n\t\t\t\t\t\tObject value = conditionMap.get(column);\n\t\t\t\t\t\tString wi = \"\";\n\t\t\t\t\t\tif (value == null && conditionMap.containsKey(column) == false) { // 兼容@null\n\t\t\t\t\t\t\tisNot = false; // 以占位表达式为准\n\t\t\t\t\t\t\tsize++; // 兼容 key 数量判断\n\t\t\t\t\t\t\twi = keyIndex > 0 ? key.substring(keyIndex + 1) : \"\";\n\t\t\t\t\t\t\tif (StringUtil.isEmpty(wi)) {\n\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\"\n\t\t\t\t\t\t\t\t\t\t+ key + \"' 对应的条件键值对 \" + column + \":value 不存在！\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\twi = isHaving ? gainHavingItem(quote, table, alias, column, (String) value, containRaw)\n\t\t\t\t\t\t\t\t\t: gainWhereItem(column, value, method, verifyName);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (1.0f*allCount/size > maxCombineRatio && maxCombineRatio > 0) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s + \"' 不合法！\"\n\t\t\t\t\t\t\t\t\t+ \"其中 key 数量 \" + allCount + \" / 条件键值对数量 \" + size + \" = \" + (1.0f*allCount/size)\n\t\t\t\t\t\t\t\t\t+ \" 已超过 最大倍数，必须在条件键值对数量 0-\" + maxCombineRatio + \" 倍内！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (StringUtil.isEmpty(wi, true)) {  // 转成 1=1 ?\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + key\n\t\t\t\t\t\t\t\t\t+ \"' 对应的 \" + column + \":value 不是有效条件键值对！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tInteger count = usedKeyCountMap.get(column);\n\t\t\t\t\t\tcount = count == null ? 1 : count + 1;\n\t\t\t\t\t\tif (count > maxCombineKeyCount && maxCombineKeyCount > 0) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s + \"' 不合法！\"\n\t\t\t\t\t\t\t\t\t+ \"其中 '\" + column + \"' 重复引用，次数 \" + count\n\t\t\t\t\t\t\t\t\t+ \" 已超过最大值，必须在 0-\" + maxCombineKeyCount + \" 内！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tusedKeyCountMap.put(column, count);\n\n\t\t\t\t\t\tresult += \"( \" + gainCondition(isNot, wi) + \" )\";\n\t\t\t\t\t\tisNot = false;\n\t\t\t\t\t\tfirst = false;\n\t\t\t\t\t}\n\n\t\t\t\t\tkey = \"\";\n\t\t\t\t\tlastLogic = 0;\n\n\t\t\t\t\tif (isOver) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (c == ' ') {\n\t\t\t\t}\n\t\t\t\telse if (c == '&') {\n\t\t\t\t\tif (last == ' ') {\n\t\t\t\t\t\tif (i >= n - 1 || s.charAt(i + 1) != ' ') {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + (i >= n - 1 ? s : s.substring(0, i + 1))\n\t\t\t\t\t\t\t\t\t+ \"' 不合法！逻辑连接符 & 右边缺少一个空格 ！逻辑连接符 & | 左右必须各一个相邻空格！空格不能多也不能少！\"\n\t\t\t\t\t\t\t\t\t+ \"不允许首尾有空格，也不允许连续空格！左括号 ( 的右边 和 右括号 ) 的左边 都不允许有相邻空格！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresult += SQL.AND;\n\t\t\t\t\t\tlastLogic = c;\n\t\t\t\t\t\ti ++;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tkey += c;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (c == '|') {\n\t\t\t\t\tif (last == ' ') {\n\t\t\t\t\t\tif (i >= n - 1 || s.charAt(i + 1) != ' ') {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ @combine: '\" + combine\n\t\t\t\t\t\t\t\t\t+ \"' } 中字符 '\" + (i >= n - 1 ? s : s.substring(0, i + 1))\n\t\t\t\t\t\t\t\t\t+ \"' 不合法！逻辑连接符 | 右边缺少一个空格 ！逻辑连接符 & | 左右必须各一个相邻空格！空格不能多也不能少！\"\n\t\t\t\t\t\t\t\t\t+ \"不允许首尾有空格，也不允许连续空格！左括号 ( 右边和右括号 ) 左边都不允许有相邻空格！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresult += SQL.OR;\n\t\t\t\t\t\tlastLogic = c;\n\t\t\t\t\t\ti ++;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tkey += c;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (c == '!') {\n\t\t\t\t\tlast = i <= 0 ? 0 : s.charAt(i - 1);  // & | 后面跳过了空格\n\n\t\t\t\t\tchar next = i >= n - 1 ? 0 : s.charAt(i + 1);\n\t\t\t\t\tif (last == ' ' || last == '(') {\n\t\t\t\t\t\tif (next == ' ') {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(0, i + 1)\n\t\t\t\t\t\t\t+ \"' 不合法！非逻辑符 '!' 右边多了一个空格 ' ' ！非逻辑符 '!' \" +\n\t\t\t\t\t\t\t\t\t\"右边不允许任何相邻空格 ' '，也不允许 ')' '&' '|' 中任何一个！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (next == ')' || next == '&' || next == '!') {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(0, i + 1)\n\t\t\t\t\t\t\t+ \"' 不合法！非逻辑符 '!' 右边多了一个字符 '\"\n\t\t\t\t\t\t\t\t\t+ next + \"' ！非逻辑符 '!' 右边不允许任何相邻空格 ' '，也不允许 ')' '&' '|' 中任何一个！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (i > 0 && lastLogic <= 0 && last != '(') {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(i)\n\t\t\t\t\t\t\t+ \"' 不合法！左边缺少 & | 逻辑连接符！逻辑连接符 & | 左右必须各一个相邻空格！空格不能多也不能少！\"\n\t\t\t\t\t\t\t+ \"不允许首尾有空格，也不允许连续空格！左括号 ( 的右边 和 右括号 ) 的左边 都不允许有相邻空格！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (next == '(') {\n\t\t\t\t\t\tresult += SQL.NOT;\n\t\t\t\t\t\tlastLogic = c;\n\t\t\t\t\t}\n\t\t\t\t\telse if (last <= 0 || last == ' ' || last == '(') {\n\t\t\t\t\t\tisNot = true;\n\t\t\t\t\t\t//\t\t\t\t\tlastLogic = c;\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tkey += c;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (c == '(') {\n\t\t\t\t\tif (key.isEmpty() == false || (i > 0 && lastLogic <= 0 && last != '(')) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(i)\n\t\t\t\t\t\t+ \"' 不合法！左边缺少 & | 逻辑连接符！逻辑连接符 & | 左右必须各一个相邻空格！空格不能多也不能少！\"\n\t\t\t\t\t\t+ \"不允许首尾有空格，也不允许连续空格！左括号 ( 的右边 和 右括号 ) 的左边 都不允许有相邻空格！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tdepth ++;\n\t\t\t\t\tif (depth > maxDepth && maxDepth > 0) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(0, i + 1)\n\t\t\t\t\t\t+ \"' 不合法！括号 (()) 嵌套层级 \" + depth + \" 已超过最大值，必须在 0-\" + maxDepth + \" 内！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tresult += c;\n\t\t\t\t\tlastLogic = 0;\n\t\t\t\t\tfirst = true;\n\t\t\t\t}\n\t\t\t\telse if (c == ')') {\n\t\t\t\t\tdepth --;\n\t\t\t\t\tif (depth < 0) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s.substring(0, i + 1)\n\t\t\t\t\t\t+ \"' 不合法！左括号 ( 比 右括号 ) 少！数量必须相等从而完整闭合 (...) ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tresult += c;\n\t\t\t\t\tlastLogic = 0;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tkey += c;\n\t\t\t\t}\n\n\t\t\t\tlast = c;\n\t\t\t\ti ++;\n\t\t\t}\n\n\t\t\tif (depth != 0) {\n\t\t\t\tthrow new IllegalArgumentException(errPrefix + \" 中字符 '\" + s\n\t\t\t\t\t\t+ \"' 不合法！左括号 ( 比 右括号 ) 多！数量必须相等从而完整闭合 (...) ！\");\n\t\t\t}\n\t\t}\n\n\t\tList<Object> exprPreparedValues = getPreparedValueList();\n\t\tif (isHaving == false) {  // 只收集 AND 条件值\n\t\t\tsetPreparedValueList(new ArrayList<>());\n\t\t}\n\n\t\tSet<Entry<String, Object>> set = conditionMap.entrySet();\n\n\t\tString andCond = \"\";\n\t\tboolean isItemFirst = true;\n\n\t\tfor (Entry<String, Object> entry : set) {\n\t\t\tString key = entry == null ? null : entry.getKey();\n\t\t\tif (key == null || usedKeyCountMap.containsKey(key)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tString wi = isHaving ? gainHavingItem(quote, table, alias, key, (String) entry.getValue(), containRaw)\n\t\t\t\t\t: gainWhereItem(key, entry.getValue(), method, verifyName);\n\t\t\tif (StringUtil.isEmpty(wi, true)) {//避免SQL条件连接错误\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tandCond += (isItemFirst ? \"\" : AND) + \"(\" + wi + \")\";\n\t\t\tisItemFirst = false;\n\t\t}\n\n\t\tif (isHaving == false) {  // 优先存放 AND 条件值\n\t\t\tpreparedValues.addAll(getPreparedValueList());\n\t\t}\n\n\t\tif (StringUtil.isEmpty(result, true)) {\n\t\t\tresult = andCond;\n\t\t}\n\t\telse if (StringUtil.isNotEmpty(andCond, true)) {  // andCond 必须放后面，否则 prepared 值顺序错误\n\t\t\tif (isHaving) {\n\t\t\t\t// HAVING 前 WHERE 已经有条件 ? 占位，不能反过来，想优化 AND 连接在最前，需要多遍历一次内部的 key，也可以 newSQLConfig<T, M, L> 时存到 andList\n\t\t\t\tresult = \"( \" + result + \" )\" + AND + andCond;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tresult = andCond + AND + \"( \" + result + \" )\";  // 先暂存之前的 prepared 值，然后反向整合\n\t\t\t}\n\t\t}\n\n\t\tif (isHaving == false) {\n\t\t\tif (exprPreparedValues != null && exprPreparedValues.isEmpty() == false) {\n\t\t\t\tpreparedValues.addAll(exprPreparedValues);  // 在 AND 条件值后存放表达式内的条件值\n\t\t\t}\n\t\t\tsetPreparedValueList(preparedValues);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**@combine:\"a,b\" 条件组合。虽然有了 @combine:\"a | b\" 这种新方式，但为了 Join 多个 On 能保证顺序正确，以及这个性能更好，还是保留这个方式\n\t * @param hasPrefix\n\t * @param method\n\t * @param where\n\t * @param combine\n\t * @param joinList\n\t * @param verifyName\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic String getWhereString(boolean hasPrefix, RequestMethod method, Map<String, Object> where\n\t\t, Map<String, List<String>> combine, List<Join<T, M, L>> joinList, boolean verifyName) throws Exception {\n\t\tSet<Entry<String, List<String>>> combineSet = combine == null ? null : combine.entrySet();\n\t\tif (combineSet == null || combineSet.isEmpty()) {\n\t\t\tLog.w(TAG, \"getWhereString  combineSet == null || combineSet.isEmpty() >> return \\\"\\\";\");\n\t\t\treturn \"\";\n\t\t}\n\n\t\tList<String> keyList;\n\n\t\tString whereString = \"\";\n\n\t\tboolean isCombineFirst = true;\n\t\tint logic;\n\n\t\tboolean isItemFirst;\n\t\tString c;\n\t\tString cs;\n\n\t\tfor (Entry<String, List<String>> ce : combineSet) {\n\t\t\tkeyList = ce == null ? null : ce.getValue();\n\t\t\tif (keyList == null || keyList.isEmpty()) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\"|\".equals(ce.getKey())) {\n\t\t\t\tlogic = Logic.TYPE_OR;\n\t\t\t}\n\t\t\telse if (\"!\".equals(ce.getKey())) {\n\t\t\t\tlogic = Logic.TYPE_NOT;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tlogic = Logic.TYPE_AND;\n\t\t\t}\n\n\t\t\tisItemFirst = true;\n\t\t\tcs = \"\";\n\t\t\tfor (String key : keyList) {\n\t\t\t\tc = gainWhereItem(key, where.get(key), method, verifyName);\n\n\t\t\t\tif (StringUtil.isEmpty(c, true)) {//避免SQL条件连接错误\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tcs += (isItemFirst ? \"\" : (Logic.isAnd(logic) ? AND : OR)) + \"(\" + c + \")\";\n\t\t\t\tisItemFirst = false;\n\t\t\t}\n\n\t\t\tif (StringUtil.isEmpty(cs, true)) {//避免SQL条件连接错误\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\twhereString += (isCombineFirst ? \"\" : AND) + (Logic.isNot(logic) ? NOT : \"\") + \" (  \" + cs + \"  ) \";\n\t\t\tisCombineFirst = false;\n\t\t}\n\n\t\twhereString = concatJoinWhereString(whereString);\n\n\t\tString s = StringUtil.isEmpty(whereString, true) ? \"\" : (hasPrefix ? \" WHERE \" : \"\") + whereString;\n\n\t\tif (s.isEmpty() && RequestMethod.isQueryMethod(method) == false) {\n\t\t\tthrow new UnsupportedOperationException(\"写操作请求必须带条件！！！\");\n\t\t}\n\n\t\treturn s;\n\t}\n\n\n\tprotected String concatJoinWhereString(String whereString) throws Exception {\n\t\tList<Join<T, M, L>> joinList = getJoinList();\n\t\tif (joinList != null) {\n\n\t\t\tString newWs = \"\";\n\t\t\tString ws = whereString;\n\n\t\t\tList<Object> newPvl = new ArrayList<>();\n\t\t\tList<Object> pvl = new ArrayList<>(getPreparedValueList());\n\n\t\t\tSQLConfig<T, M, L> jc;\n\t\t\tSQLConfig<T, M, L> outerConfig;\n\t\t\tString js;\n\t\t\tboolean isWsEmpty = StringUtil.isEmpty(ws, true);\n\t\t\tboolean changed = false;\n\t\t\t// 各种 JOIN 没办法统一用 & | ！连接，只能按优先级，和 @combine 一样?\n\t\t\tfor (Join<T, M, L> j : joinList) {\n\t\t\t\tString jt = j.getJoinType();\n\n\t\t\t\tswitch (jt) {\n\t\t\t\tcase \"*\": // CROSS JOIN\n\t\t\t\tcase \"@\": // APP JOIN\n\t\t\t\tcase \"<\": // LEFT JOIN\n\t\t\t\tcase \">\": // RIGHT JOIN\n\t\t\t\t\touterConfig = j.getOuterConfig();\n\t\t\t\t\tif (outerConfig == null){\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tboolean isMain1 = outerConfig.isMain();\n\t\t\t\t\touterConfig.setMain(false).setPrepared(isPrepared()).setPreparedValueList(new ArrayList<Object>());\n\t\t\t\t\tString outerWhere = outerConfig.gainWhereString(false);\n\n\t\t\t\t\tint logic1 = Logic.getType(jt);\n\t\t\t\t\tnewWs += \" ( \"\n\t\t\t\t\t\t\t+ gainCondition(\n\t\t\t\t\t\t\tLogic.isNot(logic1),\n\t\t\t\t\t\t\tws\n\t\t\t\t\t\t\t\t\t+ ( isWsEmpty ? \"\" : (Logic.isAnd(logic1) ? AND : OR) )\n\t\t\t\t\t\t\t\t\t+ \" ( \" + outerWhere + \" ) \"\n\t\t\t\t\t)\n\t\t\t\t\t\t\t+ \" ) \";\n\t\t\t\t\tnewPvl.addAll(pvl);\n\t\t\t\t\tnewPvl.addAll(outerConfig.getPreparedValueList());\n\n\t\t\t\t\tchanged = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"&\": // INNER JOIN: A & B\n\t\t\t\tcase \"\":  // FULL JOIN: A | B\n\t\t\t\tcase \"|\": // FULL JOIN: A | B\n\t\t\t\tcase \"!\": // OUTER JOIN: ! (A | B)\n\t\t\t\tcase \"^\": // SIDE JOIN: ! (A & B)\n\t\t\t\tcase \"(\": // ANTI JOIN: A & ! B\n\t\t\t\tcase \")\": // FOREIGN JOIN: B & ! A\n\t\t\t\tcase \"~\": // ASOF JOIN: B ~= A\n\t\t\t\t\tjc = j.getJoinConfig();\n\t\t\t\t\tboolean isMain = jc.isMain();\n\t\t\t\t\tjc.setMain(false).setPrepared(isPrepared()).setPreparedValueList(new ArrayList<Object>());\n\t\t\t\t\tjs = jc.gainWhereString(false);\n\t\t\t\t\tjc.setMain(isMain);\n\n\t\t\t\t\tboolean isOuterJoin = \"!\".equals(jt);\n\t\t\t\t\tboolean isSideJoin = \"^\".equals(jt);\n\t\t\t\t\tboolean isAntiJoin = \"(\".equals(jt);\n\t\t\t\t\tboolean isForeignJoin = \")\".equals(jt);\n\t\t\t\t\t//boolean isWsEmpty = StringUtil.isEmpty(ws, true);\n\n\t\t\t\t\tif (isWsEmpty) {\n\t\t\t\t\t\tif (isOuterJoin) { // ! OUTER JOIN: ! (A | B)\n\t\t\t\t\t\t\tthrow new NotExistException(\"no result for ! OUTER JOIN( ! (A | B) ) when A or B is empty!\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isForeignJoin) { // ) FOREIGN JOIN: B & ! A\n\t\t\t\t\t\t\tthrow new NotExistException(\"no result for ) FOREIGN JOIN( B & ! A ) when A is empty!\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (StringUtil.isEmpty(js, true)) {\n\t\t\t\t\t\tif (isOuterJoin) { // ! OUTER JOIN: ! (A | B)\n\t\t\t\t\t\t\tthrow new NotExistException(\"no result for ! OUTER JOIN( ! (A | B) ) when A or B is empty!\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (isAntiJoin) { // ( ANTI JOIN: A & ! B\n\t\t\t\t\t\t\tthrow new NotExistException(\"no result for ( ANTI JOIN( A & ! B ) when B is empty!\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (isWsEmpty) {\n\t\t\t\t\t\t\tif (isSideJoin) {\n\t\t\t\t\t\t\t\tthrow new NotExistException(\"no result for ^ SIDE JOIN( ! (A & B) ) when both A and B are empty!\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tif (isSideJoin || isForeignJoin) {\n\t\t\t\t\t\t\t\tnewWs += \" ( \" + gainCondition(true, ws) + \" ) \";\n\n\t\t\t\t\t\t\t\tnewPvl.addAll(pvl);\n\t\t\t\t\t\t\t\tnewPvl.addAll(jc.getPreparedValueList());\n\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (StringUtil.isNotEmpty(newWs, true)) {\n\t\t\t\t\t\tnewWs += AND;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (isAntiJoin) { // ( ANTI JOIN: A & ! B\n\t\t\t\t\t\tnewWs += \" ( \" + ( isWsEmpty ? \"\" : ws + AND ) + NOT + \" ( \" + js + \" ) \" + \" ) \";\n\t\t\t\t\t}\n\t\t\t\t\telse if (isForeignJoin) { // ) FOREIGN JOIN: (! A) & B  // preparedValueList.add 不好反过来  B & ! A\n\t\t\t\t\t\tnewWs += \" ( \" + NOT + \" ( \" + ws + \" ) ) \" + AND + \" ( \" + js + \" ) \";\n\t\t\t\t\t}\n\t\t\t\t\telse if (isSideJoin) { // ^ SIDE JOIN:  ! (A & B)\n\t\t\t\t\t\t//MySQL 因为 NULL 值处理问题，(A & ! B) | (B & ! A) 与 ! (A & B) 返回结果不一样，后者往往更多\n\t\t\t\t\t\tnewWs += \" ( \" + gainCondition(\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\t( isWsEmpty ? \"\" : ws + AND ) + \" ( \" + js + \" ) \"\n\t\t\t\t\t\t\t\t) + \" ) \";\n\t\t\t\t\t}\n\t\t\t\t\telse {  // & INNER JOIN: A & B; | FULL JOIN: A | B; OUTER JOIN: ! (A | B)\n\t\t\t\t\t\tint logic = Logic.getType(jt);\n\t\t\t\t\t\tnewWs += \" ( \"\n\t\t\t\t\t\t\t\t+ gainCondition(\n\t\t\t\t\t\t\t\t\t\tLogic.isNot(logic),\n\t\t\t\t\t\t\t\t\t\tws\n\t\t\t\t\t\t\t\t\t\t+ ( isWsEmpty ? \"\" : (Logic.isAnd(logic) ? AND : OR) )\n\t\t\t\t\t\t\t\t\t\t+ \" ( \" + js + \" ) \"\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t+ \" ) \";\n\t\t\t\t\t}\n\n\t\t\t\t\tnewPvl.addAll(pvl);\n\t\t\t\t\tnewPvl.addAll(jc.getPreparedValueList());\n\n\t\t\t\t\tchanged = true;\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthrow new UnsupportedOperationException(\n\t\t\t\t\t\t\t\"join:value 中 value 里的 \" + jt + \"/\" + j.getPath()\n\t\t\t\t\t\t\t+ \"错误！不支持 \" + jt + \" 等 [ @ APP, < LEFT, > RIGHT, * CROSS\"\n\t\t\t\t\t\t\t+ \", & INNER, | FULL, ! OUTER, ^ SIDE, ( ANTI, ) FOREIGN, ~ ASOF ] 之外的 JOIN 类型 !\"\n\t\t\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (changed) {\n\t\t\t\twhereString = newWs;\n\t\t\t\tsetPreparedValueList(newPvl);\n\t\t\t}\n\t\t}\n\n\t\treturn whereString;\n\t}\n\n\n\t/**\n\t * @param key\n\t * @param value\n\t * @param method\n\t * @param verifyName\n\t * @return\n\t * @throws Exception\n\t */\n\tprotected String gainWhereItem(String key, Object value, RequestMethod method, boolean verifyName) throws Exception {\n\t\tLog.d(TAG, \"getWhereItem  key = \" + key);\n\t\t// 避免筛选到全部\tvalue = key == null ? null : where.get(key);\n\t\tif (key == null || key.endsWith(\"()\") || key.startsWith(\"@\")) { //关键字||方法, +或-直接报错\n\t\t\tLog.d(TAG, \"getWhereItem  key == null || key.endsWith(()) || key.startsWith(@) >> continue;\");\n\t\t\treturn null;\n\t\t}\n\t\tif (key.endsWith(\"@\")) { // 引用\n\t\t\t//\tkey = key.substring(0, key.lastIndexOf(\"@\"));\n\t\t\tthrow new IllegalArgumentException(TAG + \".getWhereItem: 字符 \" + key + \" 不合法！\");\n\t\t}\n\n\t\tif (value == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tint keyType;\n\t\tif (key.endsWith(\"$\")) {\n\t\t\tkeyType = 1;\n\t\t}\n\t\telse if (key.endsWith(\"~\")) {\n\t\t\tkeyType = key.charAt(key.length() - 2) == '*' ? -2 : 2;  //FIXME StringIndexOutOfBoundsException\n\t\t}\n\t\telse if (key.endsWith(\"%\")) {\n\t\t\tkeyType = 3;\n\t\t}\n\t\telse if (key.endsWith(\"{}\")) {\n\t\t\tkeyType = 4;\n\t\t}\n\t\telse if (key.endsWith(\"}{\")) {\n\t\t\tkeyType = 5;\n\t\t}\n\t\telse if (key.endsWith(\"<>\")) {\n\t\t\tkeyType = 6;\n\t\t}\n\t\telse if (key.endsWith(\">=\")) {\n\t\t\tkeyType = 7;\n\t\t}\n\t\telse if (key.endsWith(\"<=\")) {\n\t\t\tkeyType = 8;\n\t\t}\n\t\telse if (key.endsWith(\">\")) {\n\t\t\tkeyType = 9;\n\t\t}\n\t\telse if (key.endsWith(\"<\")) {\n\t\t\tkeyType = 10;\n\t\t} else {  // else绝对不能省，避免再次踩坑！ keyType = 0; 写在for循环外面都没注意！\n\t\t\tkeyType = 0;\n\t\t}\n\n\t\tString column = gainRealKey(method, key, false, true, verifyName);\n\n\t\t// 原始 SQL 片段\n\t\tString rawSQL = gainRawSQL(key, value);\n\n\t\tswitch (keyType) {\n\t\tcase 1:\n\t\t\treturn gainSearchString(key, column, value, rawSQL);\n\t\tcase -2:\n\t\tcase 2:\n\t\t\treturn gainRegExpString(key, column, value, keyType < 0, rawSQL);\n\t\tcase 3:\n\t\t\treturn gainBetweenString(key, column, value, rawSQL);\n\t\tcase 4:\n\t\t\treturn gainRangeString(key, column, value, rawSQL);\n\t\tcase 5:\n\t\t\treturn gainExistsString(key, column, value, rawSQL);\n\t\tcase 6:\n\t\t\treturn gainContainString(key, column, value, rawSQL);\n\t\tcase 7:\n\t\t\treturn gainCompareString(key, column, value, \">=\", rawSQL);\n\t\tcase 8:\n\t\t\treturn gainCompareString(key, column, value, \"<=\", rawSQL);\n\t\tcase 9:\n\t\t\treturn gainCompareString(key, column, value, \">\", rawSQL);\n\t\tcase 10:\n\t\t\treturn gainCompareString(key, column, value, \"<\", rawSQL);\n\t\tdefault:  // TODO MySQL JSON类型的字段对比 key='[]' 会无结果！ key LIKE '[1, 2, 3]'  //TODO MySQL , 后面有空格！\n\t\t\treturn gainEqualString(key, column, value, rawSQL);\n\t\t}\n\t}\n\n\n\tpublic String gainEqualString(String key, String column, Object value, String rawSQL) throws Exception {\n\t\tif (value != null && JSON.isBoolOrNumOrStr(value) == false && value instanceof Subquery == false) {\n\t\t\tthrow new IllegalArgumentException(key + \":value 中value不合法！非PUT请求只支持 [Boolean, Number, String] 内的类型 ！\");\n\t\t}\n\n\t\tboolean not = column.endsWith(\"!\"); // & | 没有任何意义，写法多了不好控制\n\t\tif (not) {\n\t\t\tcolumn = column.substring(0, column.length() - 1);\n\t\t}\n\n\t\tString rc = column.endsWith(\"[\") || column.endsWith(\"{\") ? column.substring(0, column.length() - 1) : column;\n\t\tif (StringUtil.isName(rc) == false) {\n\t\t\tthrow new IllegalArgumentException(key + \":value 中key不合法！不支持 ! 以外的逻辑符 ！\");\n\t\t}\n\n\t\tString logic = value == null && rawSQL == null ? (not ? SQL.IS_NOT : SQL.IS) : (not ? \" != \" : \" = \");\n\t\treturn gainKey(column) + logic + (value instanceof Subquery ? gainSubqueryString((Subquery<T, M, L>) value)\n\t\t\t\t: (rawSQL != null ? rawSQL : gainValue(key, column, value)));\n\t}\n\n\tpublic String gainCompareString(String key, String column, Object value, String type, String rawSQL) throws Exception {\n\t\tif (value != null && JSON.isBoolOrNumOrStr(value) == false && value instanceof Subquery == false) {\n\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 不合法！比较运算 [>, <, >=, <=] 只支持 [Boolean, Number, String] 内的类型 ！\");\n\t\t}\n\n\t\tString rc = column.endsWith(\"[\") || column.endsWith(\"{\") ? column.substring(0, column.length() - 1) : column;\n\t\tif ( ! StringUtil.isName(rc)) {\n\t\t\tthrow new IllegalArgumentException(key + \":value 中 key 不合法！比较运算 [>, <, >=, <=] 不支持 [&, !, |] 中任何逻辑运算符 ！\");\n\t\t}\n\n\t\treturn gainKey(column) + \" \" + type + \" \" + (value instanceof Subquery ? gainSubqueryString((Subquery<T, M, L>) value)\n\t\t\t\t: (rawSQL != null ? rawSQL : gainValue(key, column, value)));\n\t}\n\n\tpublic String gainKey(@NotNull String key) {\n\t\tString lenFun = \"\";\n\t\tif (key.endsWith(\"[\")) {\n\t\t\tlenFun = isSQLServer() ? \"datalength\" : \"length\";\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (key.endsWith(\"{\")) {\n\t\t\tlenFun = \"json_length\";\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (isTest()) {\n\t\t\tif (key.contains(\"'\")) {  // || key.contains(\"#\") || key.contains(\"--\")) {\n\t\t\t\tthrow new IllegalArgumentException(\"参数 \" + key + \" 不合法！key 中不允许有单引号 ' ！\");\n\t\t\t}\n\t\t\treturn gainSQLValue(key).toString();\n\t\t}\n\n\t\tMap<String, String> keyMap = getKeyMap();\n\t\tString expression = keyMap == null ? null : keyMap.get(key);\n\t\tif (expression == null) {\n\t\t\texpression = COLUMN_KEY_MAP == null ? null : COLUMN_KEY_MAP.get(key);\n\t\t}\n\n\t\tString sqlKey;\n\t\tif (expression == null) {\n\t\t\tsqlKey = gainSQLKey(key);\n\t\t}\n\t\telse {\n\t\t\t// (name,tag) left(date,4) 等\n\t\t\tList<String> raw = getRaw();\n\t\t\tsqlKey = parseSQLExpression(KEY_KEY, expression, raw != null && raw.contains(KEY_KEY), false);\n\t\t}\n\n\t\treturn lenFun.isEmpty() ? sqlKey : lenFun + \"(\" + sqlKey + \")\";\n\t}\n\tpublic String gainSQLKey(String key) {\n\t\tString q = getQuote();\n\t\treturn (isKeyPrefix() ? q + gainSQLAlias() + q + \".\" : \"\") + q + key + q;\n\t}\n\n\t/**\n\t * 使用prepareStatement预编译，值为 ? ，后续动态set进去\n\t */\n\tprotected Object gainValue(@NotNull Object value) {\n\t\treturn gainValue(null, null, value);\n\t}\n\n\tprotected List<Object> preparedValueList = new ArrayList<>();\n\tprotected Object gainValue(String key, String column, Object value) {\n\t\tif (isPrepared()) {\n\t\t\tif (value == null) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tMap<String, String> castMap = getCast();\n\t\t\tString type = key == null || castMap == null ? null : castMap.get(key);\n\n\t\t\t//\t\t\tif (\"DATE\".equalsIgnoreCase(type) && value instanceof Date == false) {\n\t\t\t//\t\t\t\tvalue = value instanceof Number ? new Date(((Number) value).longValue()) : Date.valueOf((String) value);\n\t\t\t//\t\t\t}\n\t\t\t//\t\t\telse if (\"TIME\".equalsIgnoreCase(type) && value instanceof Time == false) {\n\t\t\t//\t\t\t\tvalue = value instanceof Number ? new Time(((Number) value).longValue()) : Time.valueOf((String) value);\n\t\t\t//\t\t\t}\n\t\t\t//\t\t\telse if (\"TIMESTAMP\".equalsIgnoreCase(type) && value instanceof Timestamp == false) {\n\t\t\t//\t\t\t\tvalue = value instanceof Number ? new Timestamp(((Number) value).longValue()) : Timestamp.valueOf((String) value);\n\t\t\t//\t\t\t}\n\t\t\t//\t\t\telse if (\"ARRAY\".equalsIgnoreCase(type) && value instanceof Array == false) {\n\t\t\t//\t\t\t\tvalue = ((Collection<?>) value).toArray();\n\t\t\t//\t\t\t}\n\t\t\t//\t\t\telse if (StringUtil.isEmpty(type, true) == false) {\n\t\t\t//\t\t\t\tpreparedValueList.add(value);\n\t\t\t//\t\t\t\treturn \"cast(?\" + SQL.AS + type + \")\";\n\t\t\t//\t\t\t}\n\n\t\t\tpreparedValueList.add(value);\n\t\t\treturn StringUtil.isEmpty(type, true) ? \"?\" : \"cast(?\" + SQL.AS + type + \")\";\n\t\t}\n\n\t\treturn key == null ? gainSQLValue(value) : gainSQLValue(key, column, value);\n\t}\n\n\tpublic Object gainSQLValue(String key, String column, @NotNull Object value) {\n\t\tMap<String, String> castMap = getCast();\n\t\tString type = key == null || castMap == null ? null : castMap.get(key);\n\t\tObject val = gainSQLValue(value);\n\t\treturn StringUtil.isEmpty(type, true) ? val : \"cast(\" + val + SQL.AS + type + \")\";\n\t}\n\tpublic Object gainSQLValue(@NotNull Object value) {\n\t\tif (value == null) {\n\t\t\treturn SQL.NULL;\n\t\t}\n\t\t//\treturn (value instanceof Number || value instanceof Boolean)\n\t\t//\t&& DATABASE_POSTGRESQL.equals(getDatabase()) ? value :  \"'\" + value + \"'\";\n\t\treturn (value instanceof Number || value instanceof Boolean)\n\t\t\t\t? value :  \"'\" + value.toString().replaceAll(\"\\\\'\", \"\\\\\\\\'\") + \"'\"; // MySQL 隐式转换用不了索引\n\t}\n\n\t@Override\n\tpublic List<Object> getPreparedValueList() {\n\t\treturn preparedValueList;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setPreparedValueList(List<Object> preparedValueList) {\n\t\tthis.preparedValueList = preparedValueList;\n\t\treturn this;\n\t}\n\n\t//$ search <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**search key match value\n\t* @param key\n\t* @param column\n\t* @param value\n\t* @param rawSQL\n\t* @return {@link #gainSearchString(String, String, Object[], int)}\n\t* @throws IllegalArgumentException\n\t*/\n\tpublic String gainSearchString(String key, String column, Object value, String rawSQL) throws IllegalArgumentException {\n\t\tif (rawSQL != null) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 中 \"\n\t\t\t\t\t+ key + \" 不合法！@raw 不支持 key$ 这种功能符 ！只支持 key, key!, key<, key{} 等比较运算 和 @column, @having ！\");\n\t\t}\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tcolumn = logic.getKey();\n\t\tLog.i(TAG, \"getSearchString column = \" + column);\n\n\t\tList<Object> arr = newJSONArray(value);\n\t\tif (arr.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn gainSearchString(key, column, arr.toArray(), logic.getType());\n\t}\n\t/**search key match values\n\t* @param key\n\t* @param column\n\t* @param values\n\t* @param type\n\t* @return LOGIC [  key LIKE 'values[i]' ]\n\t* @throws IllegalArgumentException\n\t*/\n\tpublic String gainSearchString(String key, String column, Object[] values, int type) throws IllegalArgumentException {\n\t\tif (values == null || values.length <= 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tString condition = \"\";\n\t\tfor (int i = 0; i < values.length; i++) {\n\t\t\tObject v = values[i];\n\t\t\tif (v instanceof String == false) {\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 的类型只能为 String 或 String[]！\");\n\t\t\t}\n\t\t\tif (((String) v).isEmpty()) {  // 允许查空格 StringUtil.isEmpty((String) v, true)\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 值 \" + v + \"是空字符串，没有意义，不允许这样传！\");\n\t\t\t}\n\t\t\t//\t\t\tif (((String) v).contains(\"%%\")) {  // 需要通过 %\\%% 来模糊搜索 %\n\t\t\t//\t\t\t\tthrow new IllegalArgumentException(key + \"$:value 中 value 值 \" + v + \" 中包含 %% ！不允许有连续的 % ！\");\n\t\t\t//\t\t\t}\n\n\t\t\tcondition += (i <= 0 ? \"\" : (Logic.isAnd(type) ? AND : OR)) + gainLikeString(key, column, (String) v);\n\t\t}\n\n\t\treturn gainCondition(Logic.isNot(type), condition);\n\t}\n\n\t/**WHERE key LIKE 'value'\n\t * @param key\n\t * @param column\n\t * @param value\n\t * @return key LIKE 'value'\n\t */\n\tpublic String gainLikeString(@NotNull String key, @NotNull String column, String value) {\n\t\tString k = key.substring(0, key.length() - 1);\n\t\tchar r = k.charAt(k.length() - 1);\n\n\t\tchar l;\n\t\tif (r == '%' || r == '_' || r == '?') {\n\t\t\tk = k.substring(0, k.length() - 1);\n\n\t\t\tl = k.charAt(k.length() - 1);\n\t\t\tif (l == '%' || l == '_' || l == '?') {\n\t\t\t\tif (l == r) {\n\t\t\t\t\tthrow new IllegalArgumentException(key + \":value 中字符 \"\n\t\t\t\t\t\t\t+ k + \" 不合法！key$:value 中不允许 key 中有连续相同的占位符！\");\n\t\t\t\t}\n\n\t\t\t\tk = k.substring(0, k.length() - 1);\n\t\t\t}\n\t\t\telse if (l > 0 && StringUtil.isName(String.valueOf(l))) {\n\t\t\t\tl = r;\n\t\t\t}\n\n\t\t\tif (l == '?') {\n\t\t\t\tl = 0;\n\t\t\t}\n\t\t\tif (r == '?') {\n\t\t\t\tr = 0;\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tl = r = 0;\n\t\t}\n\n\t\tif (l > 0 || r > 0) {\n\t\t\tif (value == null) {\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 为 null！\" +\n\t\t\t\t\t\t\"key$:value 中 value 不能为 null，且类型必须是 String ！\");\n\t\t\t}\n\n\t\t\tvalue = value.replaceAll(\"\\\\\\\\\", \"\\\\\\\\\\\\\\\\\");\n\t\t\tvalue = value.replaceAll(\"\\\\%\", \"\\\\\\\\%\");\n\t\t\tvalue = value.replaceAll(\"\\\\_\", \"\\\\\\\\_\");\n\t\t\tif (l > 0) {\n\t\t\t\tvalue = l + value;\n\t\t\t}\n\t\t\tif (r > 0) {\n\t\t\t\tvalue = value + r;\n\t\t\t}\n\t\t}\n\n\t\treturn gainKey(column) + \" LIKE \"  + gainValue(key, column, value);\n\t}\n\n\t//$ search >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//~ regexp <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**search key match RegExp values\n\t * @param key\n\t * @param column\n\t * @param value\n\t * @param ignoreCase\n\t * @return {@link #gainRegExpString(String, String, Object[], int, boolean)}\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainRegExpString(String key, String column, Object value, boolean ignoreCase, String rawSQL)\n\t\t\tthrows IllegalArgumentException {\n\t\tif (rawSQL != null) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 中 \" + key + \" 不合法！@raw 不支持 key~ 这种功能符 ！\" +\n\t\t\t\t\t\"只支持 key, key!, key<, key{} 等比较运算 和 @column, @having ！\");\n\t\t}\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tcolumn = logic.getKey();\n\t\tLog.i(TAG, \"getRegExpString column = \" + column);\n\n\t\tL arr = newJSONArray(value);\n\t\tif (arr.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn gainRegExpString(key, column, arr.toArray(), logic.getType(), ignoreCase);\n\t}\n\t/**search key match RegExp values\n\t * @param key\n\t * @param values\n\t * @param type\n\t * @param ignoreCase\n\t * @return LOGIC [  key REGEXP 'values[i]' ]\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainRegExpString(String key, String column, Object[] values, int type, boolean ignoreCase)\n\t\t\tthrows IllegalArgumentException {\n\t\tif (values == null || values.length <= 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tString condition = \"\";\n\t\tfor (int i = 0; i < values.length; i++) {\n\t\t\tif (values[i] instanceof String == false) {\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中value的类型只能为String或String[]！\");\n\t\t\t}\n\t\t\tcondition += (i <= 0 ? \"\" : (Logic.isAnd(type) ? AND : OR))\n\t\t\t\t\t+ gainRegExpString(key, column, (String) values[i], ignoreCase);\n\t\t}\n\n\t\treturn gainCondition(Logic.isNot(type), condition);\n\t}\n\n\t/**WHERE key REGEXP 'value'\n\t * @param key\n\t * @param value\n\t * @param ignoreCase\n\t * @return key REGEXP 'value'\n\t */\n\tpublic String gainRegExpString(String key, String column, String value, boolean ignoreCase) {\n\t\tif (isPSQL()) {\n\t\t\treturn gainKey(column) + \" ~\" + (ignoreCase ? \"* \" : \" \") + gainValue(key, column, value);\n\t\t}\n\t\tif (isOracle() || isDameng() || isKingBase() || (isMySQL() && gainDBVersionNums()[0] >= 8)) {\n\t\t\treturn \"regexp_like(\" + gainKey(column) + \", \" + gainValue(key, column, value) + (ignoreCase ? \", 'i'\" : \", 'c'\") + \")\";\n\t\t}\n\t\tif (isPresto() || isTrino()) {\n\t\t\treturn \"regexp_like(\" + (ignoreCase ? \"lower(\" : \"\") + gainKey(column) + (ignoreCase ? \")\" : \"\")\n\t\t    + \", \" + (ignoreCase ? \"lower(\" : \"\") + gainValue(key, column, value) + (ignoreCase ? \")\" : \"\") + \")\";\n\t\t}\n\t\tif (isClickHouse()) {\n\t\t\treturn \"match(\" + (ignoreCase ? \"lower(\" : \"\") + gainKey(column) + (ignoreCase ? \")\" : \"\")\n\t\t\t\t\t+ \", \" + (ignoreCase ? \"lower(\" : \"\") + gainValue(key, column, value) + (ignoreCase ? \")\" : \"\") + \")\";\n\t\t}\n\t\tif (isElasticsearch()) {\n\t\t\treturn gainKey(column) + \" RLIKE \" + gainValue(key, column, value);\n\t\t}\n\t\tif (isHive()) {\n\t\t\treturn (ignoreCase ? \"lower(\" : \"\") + gainKey(column) + (ignoreCase ? \")\" : \"\")\n\t\t\t\t\t+ \" REGEXP \" + (ignoreCase ? \"lower(\" : \"\") + gainValue(key, column, value) + (ignoreCase ? \")\" : \"\");\n\t\t}\n\t\treturn gainKey(column) + \" REGEXP \" + (ignoreCase ? \"\" : \"BINARY \") + gainValue(key, column, value);\n\t}\n\n\n\t//~ regexp >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//% between <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t/**WHERE key BETWEEN 'start' AND 'end'\n\t * @param key\n\t * @param value 'start,end'\n\t * @return LOGIC [ key BETWEEN 'start' AND 'end' ]\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainBetweenString(String key, String column, Object value, String rawSQL) throws IllegalArgumentException {\n\t\tif (rawSQL != null) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 中 \" + key + \" 不合法！@raw 不支持 key% 这种功能符 ！\" +\n\t\t\t\t\t\"只支持 key, key!, key<, key{} 等比较运算 和 @column, @having ！\");\n\t\t}\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tcolumn = logic.getKey();\n\t\tLog.i(TAG, \"getBetweenString column = \" + column);\n\n\t\tL arr = newJSONArray(value);\n\t\tif (arr.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn gainBetweenString(key, column, arr.toArray(), logic.getType());\n\t}\n\n\t/**WHERE key BETWEEN 'start' AND 'end'\n\t * @param key\n\t * @param column\n\t * @param values ['start,end'] TODO 在 '1,2' 和 ['1,2', '3,4'] 基础上新增支持 [1, 2] 和 [[1,2], [3,4]] ？\n\t * @param type\n\t * @return LOGIC [ key BETWEEN 'start' AND 'end' ]\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainBetweenString(String key, String column, Object[] values, int type) throws IllegalArgumentException {\n\t\tif (values == null || values.length <= 0) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tString condition = \"\";\n\t\tString[] vs;\n\t\tfor (int i = 0; i < values.length; i++) {\n\t\t\tif (values[i] instanceof String == false) {\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 的类型只能为 String 或 String[] ！\");\n\t\t\t}\n\n\t\t\tvs = StringUtil.split((String) values[i]);\n\t\t\tif (vs == null || vs.length != 2) {\n\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 不合法！类型为 String 时必须包括1个逗号 , \" +\n\t\t\t\t\t\t\"且左右两侧都有值！类型为 String[] 里面每个元素要符合前面类型为 String 的规则 ！\");\n\t\t\t}\n\n\t\t\tcondition += (i <= 0 ? \"\" : (Logic.isAnd(type) ? AND : OR))\n\t\t\t\t\t+ \"(\" + gainBetweenString(key, column, vs[0], (Object) vs[1]) + \")\";\n\t\t}\n\n\t\treturn gainCondition(Logic.isNot(type), condition);\n\t}\n\n\t/**WHERE key BETWEEN 'start' AND 'end'\n\t* @return key\n\t* @param column\n\t* @param start\n\t* @param end\n\t* @return LOGIC [ key BETWEEN 'start' AND 'end' ]\n\t* @throws IllegalArgumentException\n\t*/\n\tpublic String gainBetweenString(String key, String column, Object start, Object end) throws IllegalArgumentException {\n\t\tif (JSON.isBoolOrNumOrStr(start) == false || JSON.isBoolOrNumOrStr(end) == false) {\n\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 不合法！类型为 String 时必须包括1个逗号 , \" +\n\t\t\t\t\t\"且左右两侧都有值！类型为 String[] 里面每个元素要符合前面类型为 String 的规则 ！\");\n\t\t}\n\t\treturn gainKey(column) + \" BETWEEN \" + gainValue(key, column, start) + AND + gainValue(key, column, end);\n\t}\n\n\n\t//% between >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//{} range <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\n\t/**WHERE key > 'key0' AND key <= 'key1' AND ...\n\t * @param key\n\t * @param range \"condition0,condition1...\"\n\t * @return key condition0 AND key condition1 AND ...\n\t * @throws Exception\n\t */\n\tpublic String gainRangeString(String key, String column, Object range, String rawSQL) throws Exception {\n\t\tLog.i(TAG, \"getRangeString column = \" + column);\n\t\tif (range == null) {//依赖的对象都没有给出有效值，这个存在无意义。如果是客户端传的，那就能在客户端确定了。\n\t\t\tthrow new NotExistException(TAG + \"getRangeString(\" + column + \", \" + range + \") range == null\");\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tString k = logic.getKey();\n\t\tLog.i(TAG, \"getRangeString k = \" + k);\n\n\t\tif (range instanceof List) {\n\t\t\tif (rawSQL != null) {\n\t\t\t\tthrow new UnsupportedOperationException(\"@raw:value 的 value 中 \" + key + \" 不合法！\"\n\t\t\t\t\t\t+ \"Raw SQL 不支持 key{}:[] 这种键值对！\");\n\t\t\t}\n\n\t\t\tif (logic.isOr() || logic.isNot()) {\n\t\t\t\tList<?> l = (List<?>) range;\n\t\t\t\tif (logic.isNot() && l.isEmpty()) {\n\t\t\t\t\treturn \"\"; // key!{}: [] 这个条件无效，加到 SQL 语句中 key IN() 会报错，getInString 里不好处理\n\t\t\t\t}\n\t\t\t\treturn gainKey(k) + gainInString(k, column, l.toArray(), logic.isNot());\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(key + \":[] 中 {} 前面的逻辑运算符错误！只能用'|','!'中的一种 ！\");\n\t\t}\n\t\telse if (range instanceof String) {//非Number类型需要客户端拼接成 < 'value0', >= 'value1'这种\n\t\t\tString condition = \"\";\n\t\t\tString[] cs = rawSQL != null ? null : StringUtil.split((String) range, \";\", false);\n\n\t\t\tif (rawSQL != null) {\n\t\t\t\tint index = rawSQL.indexOf(\"(\");\n\t\t\t\tcondition = (index >= 0 && index < rawSQL.lastIndexOf(\")\") ? \"\" : gainKey(k) + \" \") + rawSQL;\n\t\t\t}\n\n\t\t\tif (cs != null) {\n\t\t\t\tList<String> raw = getRaw();\n\t\t\t\tboolean containRaw = raw == null ? false : raw.contains(key);\n\t\t\t\tString lk = logic.isAnd() ? AND : OR;\n\n\t\t\t\tfor (int i = 0; i < cs.length; i++) {//对函数条件length(key)<=5这种不再在开头加key\n\t\t\t\t\tString expr = cs[i];\n\n\t\t\t\t\tif (expr.length() > 100) {\n\t\t\t\t\t\tthrow new UnsupportedOperationException(key + \":value 的 value 中字符串 \" + expr + \" 不合法！\"\n\t\t\t\t\t\t\t\t+ \"不允许传超过 100 个字符的函数或表达式！请用 @raw 简化传参！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tint index = expr == null ? -1 : expr.indexOf(\"(\");\n\t\t\t\t\tif (index >= 0) {\n\t\t\t\t\t\texpr = parseSQLExpression(key, expr, containRaw, false\n\t\t\t\t\t\t\t\t, key + \":\\\"!=null;+3*2<=10;function0(arg0,arg1,...)>1;function1(...)%5<=3...\\\"\");\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tString fk = gainKey(k) + \" \";\n\t\t\t\t\t\tString[] ccs = StringUtil.split(expr, false);\n\t\t\t\t\t\texpr = \"\";\n\n\t\t\t\t\t\tfor (int j = 0; j < ccs.length; j++) {\n\t\t\t\t\t\t\tString c = ccs[j];\n\t\t\t\t\t\t\tif (\"=null\".equals(c)) {\n\t\t\t\t\t\t\t\tc = SQL.isNull();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (\"!=null\".equals(c)) {\n\t\t\t\t\t\t\t\tc = SQL.isNull(false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (isPrepared() && (c.contains(\"--\") || PATTERN_RANGE.matcher(c).matches() == false)) {\n\t\t\t\t\t\t\t\tthrow new UnsupportedOperationException(key + \":value 的 value 中 \" + c + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t+ \"预编译模式下 key{}:\\\"condition\\\" 中 condition 必须 为 =null 或 !=null \" +\n\t\t\t\t\t\t\t\t\t\t\"或 符合正则表达式 \" + PATTERN_RANGE + \" ！不允许连续减号 -- ！不允许空格！\");\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\texpr += (j <= 0 ? \"\" : lk) + fk + c;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tcondition += ((i <= 0 ? \"\" : lk) + expr);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (condition.isEmpty()) {\n\t\t\t\treturn \"\";\n\t\t\t}\n\n\t\t\treturn gainCondition(logic.isNot(), condition);\n\t\t}\n\t\telse if (range instanceof Subquery) {\n\t\t\t// 如果在 Parser<T, M, L> 解析成 SQL 字符串再引用，没法保证安全性，毕竟可以再通过远程函数等方式来拼接再替代，最后引用的字符串就能注入\n\t\t\treturn gainKey(k) + (logic.isNot() ? NOT : \"\") + \" IN \" + gainSubqueryString((Subquery) range);\n\t\t}\n\n\t\tthrow new IllegalArgumentException(key + \":range 类型为\" + range.getClass().getSimpleName()\n\t\t\t\t+ \"！range 只能是 用','分隔条件的字符串 或者 可取选项JSONArray！\");\n\t}\n\t/**WHERE key IN ('key0', 'key1', ... )\n\t * @param in\n\t * @return IN ('key0', 'key1', ... )\n\t * @throws NotExistException\n\t */\n\tpublic String gainInString(String key, String column, Object[] in, boolean not) throws NotExistException {\n\t\tString condition = \"\";\n\t\tif (in != null) {//返回 \"\" 会导致 id:[] 空值时效果和没有筛选id一样！\n\t\t\tfor (int i = 0; i < in.length; i++) {\n\t\t\t\tcondition += ((i > 0 ? \",\" : \"\") + gainValue(key, column, in[i]));\n\t\t\t}\n\t\t}\n\t\tif (condition.isEmpty()) {//条件如果存在必须执行，不能忽略。条件为空会导致出错，又很难保证条件不为空(@:条件)，所以还是这样好\n\t\t\tthrow new NotExistException(TAG + \".getInString(\" + key + \",\" + column + \", [], \" + not + \") >> condition.isEmpty() >> IN()\");\n\t\t}\n\t\treturn (not ? NOT : \"\") + \" IN (\" + condition + \")\";\n\t}\n\t//{} range >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//}{ exists <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**WHERE EXISTS subquery\n\t * 如果合并到 getRangeString，一方面支持不了 [1,2,2] 和 \">1\" (转成 EXISTS(SELECT IN ) 需要\n\t * static newSQLConfig，但它不能传入子类实例，除非不是 static)，另一方面多了子查询临时表性能会比 IN 差\n\t * @param key\n\t * @param value\n\t * @return EXISTS ALL(SELECT ...)\n\t * @throws NotExistException\n\t */\n\tpublic String gainExistsString(String key, String column, Object value, String rawSQL) throws Exception {\n\t\tif (rawSQL != null) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 中 \" + key + \" 不合法！\" +\n\t\t\t\t\t\"@raw 不支持 key}{ 这种功能符 ！只支持 key, key!, key<, key{} 等比较运算 和 @column, @having ！\");\n\t\t}\n\t\tif (value == null) {\n\t\t\treturn \"\";\n\t\t}\n\t\tif (value instanceof Subquery == false) {\n\t\t\tthrow new IllegalArgumentException(key + \":subquery 类型为\" + value.getClass().getSimpleName()\n\t\t\t\t\t+ \"！subquery 只能是 子查询JSONObejct！\");\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tcolumn = logic.getKey();\n\t\tLog.i(TAG, \"getExistsString column = \" + column);\n\n\t\treturn (logic.isNot() ? NOT : \"\") + \" EXISTS \" + gainSubqueryString((Subquery) value);\n\t}\n\t//}{ exists >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t//<> contain <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**WHERE key contains value\n\t * @param key\n\t * @param value\n\t * @return\t{@link #gainContainString(String, String, Object[], int)}\n\t * @throws NotExistException\n\t */\n\tpublic String gainContainString(String key, String column, Object value, String rawSQL) throws IllegalArgumentException {\n\t\tif (rawSQL != null) {\n\t\t\tthrow new UnsupportedOperationException(\"@raw:value 中 \" + key + \" 不合法！@raw 不支持 key<> 这种功能符 ！\" +\n\t\t\t\t\t\"只支持 key, key!, key<, key{} 等比较运算 和 @column, @having ！\");\n\t\t}\n\n\t\tLogic logic = new Logic(column);\n\t\tcolumn = logic.getKey();\n\t\tLog.i(TAG, \"getContainString column = \" + column);\n\n\t\treturn gainContainString(key, column, newJSONArray(value).toArray(), logic.getType());\n\t}\n\t/**WHERE key contains childs TODO 支持 key<>: { \"path\":\"$[0].name\", \"value\": 82001 }\n\t *  或者  key<$[0].name>:82001 或者  key$[0].name<>:82001 ？ 还是前者好，key 一旦复杂了，\n\t *  包含 , ; : / [] 等就容易和 @combine 其它功能等冲突\n\t * @param key\n\t * @param childs null ? \"\" : (empty ? no child : contains childs)\n\t * @param type |, &, !\n\t * @return LOGIC [  ( key LIKE '[\" + childs[i] + \"]'  OR  key LIKE '[\" + childs[i] + \", %'\n\t *   OR  key LIKE '%, \" + childs[i] + \", %'  OR  key LIKE '%, \" + childs[i] + \"]' )  ]\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainContainString(String key, String column, Object[] childs, int type) throws IllegalArgumentException {\n\t\tboolean not = Logic.isNot(type);\n\t\tString condition = \"\";\n\t\tif (childs != null) {\n\t\t\tfor (int i = 0; i < childs.length; i++) {\n\t\t\t\tObject c = childs[i];\n\t\t\t\tif (c instanceof Collection) {\n\t\t\t\t\tthrow new IllegalArgumentException(key + \":value 中 value 类型不能为 [JSONList, Collection] 中的任何一个 ！\");\n\t\t\t\t}\n\n\t\t\t\tObject path = \"\";\n\t\t\t\tif (c instanceof Map) {\n\t\t\t\t\tpath = ((Map<?, ?>) c).get(\"path\");\n\t\t\t\t\tif (path != null && path instanceof String == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(key + \":{ path:path, value:value } 中 path 类型错误，\" +\n\t\t\t\t\t\t\t\t\"只能是 $, $.key1, $[0].key2 等符合 SQL 中 JSON 路径的 String ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tc = ((Map<?, ?>) c).get(\"value\");\n\t\t\t\t\tif (c instanceof Collection || c instanceof Map) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(key + \":{ path:path, value:value } 中 value 类型\" +\n\t\t\t\t\t\t\t\t\"不能为 [JSONMap, JSONList, Collection, Map] 中的任何一个 ！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcondition += (i <= 0 ? \"\" : (Logic.isAnd(type) ? AND : OR));\n\t\t\t\tif (isPSQL()) {\n\t\t\t\t\tcondition += (gainKey(column) + \" @> \" + gainValue(key, column, newJSONArray(c)));\n\t\t\t\t\t// operator does not exist: jsonb @> character varying  \"[\" + c + \"]\");\n\t\t\t\t}\n\t\t\t\telse if (isOracle() || isDameng() || isKingBase()) {\n\t\t\t\t\tcondition += (\"json_textcontains(\" + gainKey(column) + \", \" + (StringUtil.isEmpty(path, true)\n\t\t\t\t\t\t\t? \"'$'\" : gainValue(key, column, path)) + \", \" + gainValue(key, column, c == null ? null : c.toString()) + \")\");\n\t\t\t\t}\n\t\t\t\telse if (isPresto() || isTrino()) {\n\t\t\t\t\tcondition += (\"json_array_contains(cast(\" + gainKey(column) + \" AS VARCHAR), \"\n\t\t\t\t\t\t\t+ gainValue(key, column, c) + (StringUtil.isEmpty(path, true)\n\t\t\t\t\t\t\t? \"\" : \", \" + gainValue(key, column, path)) + \")\");\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tString v = c == null ? \"null\" : (c instanceof Boolean || c instanceof Number ? c.toString() : \"\\\"\" + c + \"\\\"\");\n\t\t\t\t\tif (isClickHouse()) {\n\t\t\t\t\t\tcondition += (condition + \"has(JSONExtractArrayRaw(assumeNotNull(\" + gainKey(column) + \"))\"\n\t\t\t\t\t\t\t\t+ \", \" + gainValue(key, column, v) + (StringUtil.isEmpty(path, true)\n\t\t\t\t\t\t\t\t? \"\" : \", \" + gainValue(key, column, path)) + \")\");\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tcondition += (\"json_contains(\" + gainKey(column) + \", \" + gainValue(key, column, v)\n\t\t\t\t\t\t\t\t+ (StringUtil.isEmpty(path, true) ? \"\" : \", \" + gainValue(key, column, path)) + \")\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (condition.isEmpty()) {\n\t\t\t\tcondition = gainKey(column) + SQL.isNull(true) + OR + gainLikeString(key, column, \"[]\"); // key = '[]' 无结果！\n\t\t\t}\n\t\t\telse {\n\t\t\t\tcondition = gainKey(column) + SQL.isNull(false) + AND + \"(\" + condition + \")\";\n\t\t\t}\n\t\t}\n\n\t\tif (condition.isEmpty()) {\n\t\t\treturn \"\";\n\t\t}\n\t\treturn gainCondition(not, condition);\n\t}\n\t//<> contain >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t//key@:{} Subquery <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\tpublic List<String> getWithAsExprSQLList() {\n\t\treturn withAsExprSQLList;\n\t}\n\tprivate void clearWithAsExprListIfNeed() {\n\t\t// mysql8版本以上,子查询支持with as表达式\n\t\tif(this.isMySQL() && this.gainDBVersionNums()[0] >= 8) {\n\t\t\tthis.withAsExprSQLList = new ArrayList<>();\n\t\t}\n\t}\n\n\t@Override\n\tpublic List<Object> getWithAsExprPreparedValueList() {\n\t\treturn this.withAsExprPreparedValueList;\n\t}\n\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setWithAsExprPreparedValueList(List<Object> list) {\n\t\tthis.withAsExprPreparedValueList = list;\n\t\treturn this;\n\t}\n\n\t/**\n\t * 只要 method != RequestMethod.POST 就都支持 with-as表达式\n\t * @param cfg\n\t * @param subquery\n\t * @return\n\t * @throws Exception\n\t */\n\tprivate String withAsExprSubqueryString(SQLConfig<T, M, L> cfg, Subquery<T, M, L> subquery) throws Exception {\n\t\tboolean isWithAsEnable = isWithAsEnable();\n\t\tList<String> list = isWithAsEnable ? getWithAsExprSQLList() : null;\n\t\tif (cfg.getMethod() != RequestMethod.POST && list == null) {\n\t\t\tclearWithAsExprListIfNeed();\n\t\t}\n\n\t\tString quote = getQuote();\n\t\tString as = gainAs();\n\n\t\tString withAsExpreSql;\n\t\tif (list != null) {\n\t\t\tString withQuoteName = quote + subquery.gainKey() + quote;\n\t\t\tlist.add(\" \" + withQuoteName + as + \"(\" + cfg.gainSQL(isPrepared()) + \") \");\n\t\t\twithAsExpreSql = \" SELECT * FROM \" + withQuoteName;\n\n\t\t\t// 预编译参数 FIXME 这里重复添加了，导致子查询都报错参数超过 ? 数量 Parameter index out of range (5 > number of parameters, which is 4)\n\t\t\tList<Object> subPvl = cfg.getPreparedValueList();\n\t\t\tif (subPvl != null && subPvl.isEmpty() == false) {\n\t\t\t\tList<Object> valueList = getWithAsExprPreparedValueList();\n\t\t\t\tif (valueList == null) {\n\t\t\t\t\tvalueList = new ArrayList<>();\n\t\t\t\t}\n\t\t\t\tvalueList.addAll(subPvl);\n\t\t\t\tsetWithAsExprPreparedValueList(valueList);\n\n\t\t\t\tcfg.setPreparedValueList(new ArrayList<>());\n\t\t\t}\n\t\t} else {\n\t\t\twithAsExpreSql = cfg.gainSQL(isPrepared());\n\t\t\t// mysql 才存在这个问题, 主表和子表是一张表\n\t\t\tif (isWithAsEnable && isMySQL() && StringUtil.equals(getTable(), subquery.gainFrom())) {\n\t\t\t\twithAsExpreSql = \" SELECT * FROM (\" + withAsExpreSql + \")\" + as + quote + subquery.gainKey() + quote;\n\t\t\t}\n\t\t}\n\n\t\treturn withAsExpreSql;\n\t}\n\n\t@Override\n\tpublic String gainSubqueryString(Subquery<T, M, L>  subquery) throws Exception {\n\t\tif (subquery == null) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tString range = subquery.gainRange();\n\t\tSQLConfig<T, M, L> cfg = subquery.gainConfig();\n\n\t\t// 子查询  = 主语句 datasource\n\t\tif (StringUtil.equals(this.getTable(), subquery.gainFrom()) == false && cfg.hasJoin() == false) {\n\t\t\tcfg.setDatasource(this.getDatasource());\n\t\t}\n\t\tcfg.setPreparedValueList(new ArrayList<>());\n\t\tString withAsExprSql = withAsExprSubqueryString(cfg, subquery);\n\t\tString sql = (range  == null || range.isEmpty() ? \"\" : range) + \"(\" + withAsExprSql + \") \";\n\n\t\t//// SELECT .. FROM(SELECT ..) ..  WHERE .. 格式需要把子查询中的预编译值提前\n\t\t//// 如果外查询 SELECT concat(`name`,?)  这种 SELECT 里也有预编译值，那就不能这样简单反向\n\t\t//List<Object> subPvl = cfg.getPreparedValueList();\n\t\t//if (subPvl != null && subPvl.isEmpty() == false) {\n\t\t//  List<Object> pvl = getPreparedValueList();\n\t\t//\n\t\t//  if (pvl != null && pvl.isEmpty() == false) {\n\t\t//    subPvl.addAll(pvl);\n\t\t//  }\n\t\t//  setPreparedValueList(subPvl);\n\t\t//}\n\n\t\tList<Object> subPvl = cfg.getPreparedValueList();\n\t\tif (subPvl != null && subPvl.isEmpty() == false) {\n\t\t\tList<Object> pvl = getPreparedValueList();\n\n\t\t\tif (pvl == null || pvl.isEmpty()) {\n\t\t\t\tpvl = subPvl;\n\t\t\t}\n\t\t\telse {\n\t\t\t\tpvl.addAll(subPvl);\n\t\t\t}\n\n\t\t\tsetPreparedValueList(pvl);\n\t\t}\n\n\t\treturn sql;\n\t}\n\n\t//key@:{} Subquery >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t/**拼接条件\n\t * @param not\n\t * @param condition\n\t * @return\n\t */\n\tpublic static String gainCondition(boolean not, String condition) {\n\t\treturn gainCondition(not, condition, false);\n\t}\n\t/**拼接条件\n\t * @param not\n\t * @param condition\n\t * @param addOuterBracket\n\t * @return\n\t */\n\tpublic static String gainCondition(boolean not, String condition, boolean addOuterBracket) {\n\t\tString s = not ? NOT + \"(\" + condition + \")\" : condition;\n\t\treturn addOuterBracket ? \"( \" + s + \" )\" : s;\n\t}\n\n\n\t/**转为JSONArray\n\t * @param obj\n\t * @return\n\t */\n\t@NotNull\n\tpublic static <L extends List<Object>> L newJSONArray(Object obj) {\n\t\tL array = JSON.createJSONArray();\n\t\tif (obj != null) {\n\t\t\tif (obj instanceof Collection) {\n\t\t\t\tarray.addAll((Collection<?>) obj);\n\t\t\t} else {\n\t\t\t\tarray.add(obj);\n\t\t\t}\n\t\t}\n\t\treturn array;\n\t}\n\n\t//WHERE >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t//SET <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t/**获取SET\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic String gainSetString() throws Exception {\n\t\treturn gainSetString(getMethod(), getContent(), ! isTest());\n\t}\n\t//CS304 Issue link: https://github.com/Tencent/APIJSON/issues/48\n\t/**获取SET\n\t * @param method -the method used\n\t * @param content -the content map\n\t * @return\n\t * @throws Exception\n\t * <p>use entrySet+getValue() to replace keySet+get() to enhance efficiency</p>\n\t */\n\tpublic String gainSetString(RequestMethod method, Map<String, Object> content, boolean verifyName) throws Exception {\n\t\tSet<String> set = content == null ? null : content.keySet();\n\t\tString setString = \"\";\n\n\t\tif (set != null && set.size() > 0) {\n\t\t\tboolean isFirst = true;\n\t\t\tint keyType;// 0 - =; 1 - +, 2 - -\n\t\t\tObject value;\n\n\t\t\tString idKey = getIdKey();\n\t\t\tfor (Entry<String,Object> entry : content.entrySet()) {\n\t\t\t\tString key = entry.getKey();\n\t\t\t\t//避免筛选到全部\tvalue = key == null ? null : content.get(key);\n\t\t\t\tif (key == null || idKey.equals(key)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (key.endsWith(\"+\")) {\n\t\t\t\t\tkeyType = 1;\n\t\t\t\t} else if (key.endsWith(\"-\")) {\n\t\t\t\t\tkeyType = 2;\n\t\t\t\t} else {\n\t\t\t\t\tkeyType = 0; //注意重置类型，不然不该加减的字段会跟着加减\n\t\t\t\t}\n\t\t\t\tvalue = entry.getValue();\n\t\t\t\tString column = gainRealKey(method, key, false, true, verifyName);\n\n\t\t\t\tsetString += (isFirst ? \"\" : \", \") + (gainKey(column) + \" = \"\n\t\t\t\t\t\t+ (keyType == 1 ? gainAddString(key, column, value) : (keyType == 2\n\t\t\t\t\t\t? gainRemoveString(key, column, value) : gainValue(key, column, value)) )\n\t\t\t\t);\n\n\t\t\t\tisFirst = false;\n\t\t\t}\n\t\t}\n\n\t\tif (setString.isEmpty()) {\n\t\t\tthrow new IllegalArgumentException(\"PUT 请求必须在Table内设置要修改的 key:value ！\");\n\t\t}\n\t\treturn (isClickHouse() ? \" \" : \" SET \") + setString;\n\t}\n\n\t/**SET key = concat(key, 'value')\n\t * @param key\n\t * @param value\n\t * @return concat(key, 'value')\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainAddString(String key, String column, Object value) throws IllegalArgumentException {\n\t\tif (value instanceof Number) {\n\t\t\treturn gainKey(column) + \" + \" + value;\n\t\t}\n\t\tif (value instanceof String) {\n\t\t\treturn SQL.concat(gainKey(column), (String) gainValue(key, column, value));\n\t\t}\n\t\tthrow new IllegalArgumentException(key + \":value 中 value 类型错误，必须是 Number,String,Array 中的任何一种！\");\n\t}\n\t/**SET key = replace(key, 'value', '')\n\t * @param key\n\t * @param value\n\t * @return REPLACE (key, 'value', '')\n\t * @throws IllegalArgumentException\n\t */\n\tpublic String gainRemoveString(String key, String column, Object value) throws IllegalArgumentException {\n\t\tif (value instanceof Number) {\n\t\t\treturn gainKey(column) + \" - \" + value;\n\t\t}\n\t\tif (value instanceof String) {\n\t\t\treturn SQL.replace(gainKey(column), (String) gainValue(key, column, value), \"''\");\n\t\t\t// \" replace(\" + column + \", '\" + value + \"', '') \";\n\t\t}\n\t\tthrow new IllegalArgumentException(key + \":value 中 value 类型错误，必须是 Number,String,Array 中的任何一种！\");\n\t}\n\t//SET >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t@Override\n\tpublic boolean isFakeDelete() {\n\t\treturn false;\n\t}\n\n\t@Override\n\tpublic Map<String, Object> onFakeDelete(Map<String, Object> map) {\n\t\treturn map;\n\t}\n\n\t/**\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic String gainSQL(boolean prepared) throws Exception {\n\t\tboolean isPrepared = isPrepared();\n\t\tif (isPrepared == prepared) {\n\t\t\treturn gainSQL(this);\n\t\t}\n\n\t\tString sql = gainSQL(this.setPrepared(prepared));\n\t\tsetPrepared(isPrepared);\n\t\treturn sql;\n\t}\n\t/**\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> String gainSQL(AbstractSQLConfig<T, M, L> config) throws Exception {\n\t\tif (config == null) {\n\t\t\tLog.i(TAG, \"getSQL  config == null >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\t// TODO procedure 改为 List<Procedure>  procedureList; behind : true; function: callFunction(); String key; ...\n\t\t// for (...) { Call procedure1();\\n SQL \\n; Call procedure2(); ... }\n\t\t// 貌似不需要，因为 ObjectParser<T, M, L> 里就已经处理的顺序等，只是这里要解决下 Schema 问题。\n\n\t\tString procedure = config.getProcedure();\n\t\tif (StringUtil.isNotEmpty(procedure, true)) {\n\t\t\tint ind = procedure.indexOf(\".\");\n\t\t\tboolean hasPrefix = ind >= 0 && ind < procedure.indexOf(\"(\");\n\t\t\tString sch = hasPrefix ? AbstractFunctionParser.extractSchema(\n\t\t\t\t\tprocedure.substring(0, ind), config.getTable()\n\t\t\t) : config.gainSQLSchema();\n\n\t\t\tString q = config.getQuote();\n\t\t\treturn \"CALL \" + q + sch + q + \".\" + (hasPrefix ? procedure.substring(ind + 1) : procedure);\n\t\t}\n\n\t\tString tablePath = config.gainTablePath();\n\t\tif (StringUtil.isEmpty(tablePath, true)) {\n\t\t\tLog.i(TAG, \"getSQL  StringUtil.isEmpty(tablePath, true) >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\t// 解决重复添加导致报错：Parameter index out of range (6 > number of parameters, which is 5)\n\t\tconfig.setPreparedValueList(new ArrayList<>());\n\t\tRequestMethod method = config.getMethod();\n\t\tif (method == null) {\n\t\t\tmethod = GET;\n\t\t}\n\n\t\tString cSql = null;\n\t\tswitch (method) {\n\t\t\tcase POST:\n\t\t\t\treturn \"INSERT INTO \" + tablePath + config.gainColumnString() + \" VALUES\" + config.getValuesString();\n\t\t\tcase PUT:\n\t\t\t\tif(config.isClickHouse()){\n\t\t\t\t\treturn  \"ALTER TABLE \" +  tablePath + \" UPDATE\" + config.gainSetString() + config.gainWhereString(true);\n\t\t\t\t}\n\t\t\t\tcSql =  \"UPDATE \" + tablePath + config.gainSetString() + config.gainWhereString(true)\n\t\t\t\t\t\t+ (config.isMySQL() ? config.gainLimitString() : \"\");\n\t\t\t\tcSql = buildWithAsExprSql(config, cSql);\n\t\t\t\treturn cSql;\n\t\t\tcase DELETE:\n\t\t\t\tif(config.isClickHouse()){\n\t\t\t\t\treturn  \"ALTER TABLE \" +  tablePath + \" DELETE\" + config.gainWhereString(true);\n\t\t\t\t}\n\t\t\t\tcSql =  \"DELETE FROM \" + tablePath + config.gainWhereString(true)\n\t\t\t\t\t\t+ (config.isMySQL() ? config.gainLimitString() : \"\");  // PostgreSQL 不允许 LIMIT\n\t\t\t\tcSql = buildWithAsExprSql(config, cSql);\n\t\t\t\treturn cSql;\n\t\t\tdefault:\n\t\t\t\tString explain = config.isExplain() ? (config.isSQLServer() ? \"SET STATISTICS PROFILE ON  \"\n\t\t\t\t\t\t: (config.isOracle() || config.isDameng() || config.isKingBase() ? \"EXPLAIN PLAN FOR \" : \"EXPLAIN \")) : \"\";\n\t\t\t\tif (config.isTest() && RequestMethod.isGetMethod(config.getMethod(), true)) {  // FIXME 为啥是 code 而不是 count ？\n\t\t\t\t\tString q = config.getQuote();  // 生成 SELECT  (  (24 >=0 AND 24 <3)  )  AS `code` LIMIT 1 OFFSET 0\n\t\t\t\t\treturn explain + \"SELECT \" + config.gainWhereString(false)\n\t\t\t\t\t\t\t+ config.gainAs() + q + JSONResponse.KEY_COUNT + q + config.gainLimitString();\n\t\t\t\t}\n\n\t\t\t\tconfig.setPreparedValueList(new ArrayList<Object>());\n\t\t\t\tString column = config.gainColumnString();\n\t\t\t\tif (config.isOracle() || config.isDameng() || config.isKingBase()) {\n\t\t\t\t\t//When config's database is oracle,Using subquery since Oracle12 below does not support OFFSET FETCH paging syntax.\n\t\t\t\t\t//针对oracle分组后条数的统计\n\t\t\t\t\tif (StringUtil.isNotEmpty(config.getGroup(),true) && RequestMethod.isHeadMethod(config.getMethod(), true)){\n\t\t\t\t\t\treturn explain + \"SELECT count(*) FROM (SELECT \" + (config.getCache() == JSONMap.CACHE_RAM\n\t\t\t\t\t\t\t\t? \"SQL_NO_CACHE \" : \"\") + column + \" FROM \" + gainConditionString(tablePath, config) + \") \" + config.gainLimitString();\n\t\t\t\t\t}\n\n\t\t\t\t\tString sql = \"SELECT \" + (config.getCache() == JSONMap.CACHE_RAM\n\t\t\t\t\t\t\t? \"SQL_NO_CACHE \" : \"\") + column + \" FROM \" + gainConditionString(tablePath, config);\n\t\t\t\t\treturn explain + config.gainOraclePageSQL(sql);\n\t\t\t\t}\n\n\t\t\t\tcSql = \"SELECT \" + (config.getCache() == JSONMap.CACHE_RAM ? \"SQL_NO_CACHE \" : \"\")\n\t\t\t\t\t\t+ column + \" FROM \" + gainConditionString(tablePath, config) + config.gainLimitString();\n\t\t\t\tcSql = buildWithAsExprSql(config, cSql);\n\t\t\t\tif(config.isElasticsearch()) { // elasticSearch 不支持 explain\n\t\t\t\t\treturn cSql;\n\t\t\t\t}\n\t\t\t\treturn explain + cSql;\n\t\t}\n\t}\n\n\tprivate static <T, M extends Map<String, Object>, L extends List<Object>> String buildWithAsExprSql(@NotNull AbstractSQLConfig<T, M, L> config, String cSql) throws Exception {\n\t\tif (config.isWithAsEnable() == false) {\n\t\t\treturn cSql;\n\t\t}\n\n\t\tList<String> list = config.getWithAsExprSQLList();\n\t\tint size = list == null ? 0 : list.size();\n\t\tif (size > 0) {\n\t\t\tString withAsExpreSql = \"WITH \";\n\t\t\tfor (int i = 0; i < size; i++) {\n\t\t\t\twithAsExpreSql += (i <= 0 ? \"\" : \",\") + list.get(i) + \"\\n\";\n\t\t\t}\n\t\t\tcSql = withAsExpreSql + cSql;\n\t\t\tconfig.clearWithAsExprListIfNeed();\n\t\t}\n\t\treturn cSql;\n\t}\n\n\t@Override\n\tpublic boolean isWithAsEnable() {\n\t\treturn ENABLE_WITH_AS && (isMySQL() == false || gainDBVersionNums()[0] >= 8);\n\t}\n\n\t/**Oracle的分页获取\n\t * @param sql\n\t * @return\n\t */\n\tprotected String gainOraclePageSQL(String sql) {\n\t\tint count = getCount();\n\t\tif (count <= 0 || RequestMethod.isHeadMethod(getMethod(), true)) { // TODO HEAD 真的不需要 LIMIT ？\n\t\t\treturn sql;\n\t\t}\n\t\tint offset = getOffset(getPage(), count);\n\t\tString quote = getQuote();\n\t\tString alias = quote + gainSQLAlias() + quote;\n\t\treturn \"SELECT * FROM (SELECT \" + alias + \".*, ROWNUM \"+ quote + \"RN\" + quote +\" FROM (\" + sql + \") \" + alias\n\t\t\t\t+ \"  WHERE ROWNUM <= \" + (offset + count) + \") WHERE \"+ quote + \"RN\" + quote +\" > \" + offset;\n\t}\n\n\t/**获取条件SQL字符串\n\t * @param table\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\tprivate static <T, M extends Map<String, Object>, L extends List<Object>> String gainConditionString(\n\t\t\tString table, AbstractSQLConfig<T, M, L> config) throws Exception {\n\t\tSubquery<T, M, L> from = config.getFrom();\n\t\tif (from != null) {\n\t\t\ttable = config.gainSubqueryString(from) + config.gainAs() + config.gainSQLAliasWithQuote() + \" \";\n\t\t}\n\n\t\tString join = config.gainJoinString();\n\n\t\tString where = config.gainWhereString(true);\n\n\t\t//根据方法不同，聚合语句不同。GROUP  BY 和 HAVING 可以加在 HEAD 上, HAVING 可以加在 PUT, DELETE 上，GET 全加，POST 全都不加\n\t\tString aggregation;\n\t\tif (RequestMethod.isGetMethod(config.getMethod(), true)) {\n\t\t\taggregation = config.gainGroupString(true) + config.gainHavingString(true)\n\t\t\t\t\t+ config.gainSampleString(true) + config.gainLatestString(true)\n\t\t\t\t\t+ config.gainPartitionString(true) + config.gainFillString(true)\n\t\t\t\t\t+ config.gainOrderString(true);\n\t\t}\n\t\telse if (RequestMethod.isHeadMethod(config.getMethod(), true)) {\n\t\t\t// TODO 加参数 isPagenation 判断是 GET 内分页 query:2 查总数，不用加这些条件\n\t\t\taggregation = config.gainGroupString(true) + config.gainHavingString(true)\n\t\t\t\t\t+ config.gainSampleString(true) + config.gainLatestString(true)\n\t\t\t\t\t+ config.gainPartitionString(true) + config.gainFillString(true);\n\t\t}\n\t\telse if (config.getMethod() == PUT || config.getMethod() == DELETE) {\n\t\t\taggregation = config.gainHavingString(true) ;\n\t\t}\n\t\telse {\n\t\t\taggregation = \"\";\n\t\t}\n\n\t\tString condition = table + join + where + aggregation;\n\t\t; //+ config.getLimitString();\n\n\t\t//no need to optimize\n\t\t//\t\tif (config.getPage() <= 0 || ID.equals(column.trim())) {\n\t\treturn condition;  // config.isOracle() ? condition : condition + config.getLimitString();\n\t\t//\t\t}\n\t\t//\n\t\t//\n\t\t//\t\t//order: id+ -> id >= idOfStartIndex; id- -> id <= idOfStartIndex <<<<<<<<<<<<<<<<<<<\n\t\t//\t\tString order = StringUtil.getNoBlankString(config.getOrder());\n\t\t//\t\tList<String> orderList = order.isEmpty() ? null : Arrays.asList(StringUtil.split(order));\n\t\t//\n\t\t//\t\tint type = 0;\n\t\t//\t\tif (BaseModel.isEmpty(orderList) || BaseModel.isContain(orderList, ID+\"+\")) {\n\t\t//\t\t\ttype = 1;\n\t\t//\t\t}\n\t\t//\t\telse if (BaseModel.isContain(orderList, ID+\"-\")) {\n\t\t//\t\t\ttype = 2;\n\t\t//\t\t}\n\t\t//\n\t\t//\t\tif (type > 0) {\n\t\t//\t\t\treturn condition.replace(\"WHERE\",\n\t\t//\t\t\t\t\t\"WHERE id \" + (type == 1 ? \">=\" : \"<=\") + \" (SELECT id FROM \" + table\n\t\t//\t\t\t\t\t+ where + \" ORDER BY id \" + (type == 1 ? \"ASC\" : \"DESC\") + \" LIMIT \" + config.getOffset() + \", 1) AND\"\n\t\t//\t\t\t\t\t)\n\t\t//\t\t\t\t\t+ \" LIMIT \" + config.getCount(); //子查询起始id不一定准确，只能作为最小可能！ ;//\n\t\t//\t\t}\n\t\t//\t\t//order: id+ -> id >= idOfStartIndex; id- -> id <= idOfStartIndex >>>>>>>>>>>>>>>>>>\n\t\t//\n\t\t//\n\t\t//\t\t//结果错误！SELECT * FROM User AS t0 INNER JOIN\n\t\t//      (SELECT id FROM User ORDER BY date ASC LIMIT 20, 10) AS t1 ON t0.id = t1.id\n\t\t//\t\t//common case, inner join\n\t\t//\t\tcondition += config.getLimitString();\n\t\t//\t\treturn table + \" AS t0 INNER JOIN (SELECT id FROM \" + condition + \") AS t1 ON t0.id = t1.id\";\n\t}\n\n\n\tprivate boolean keyPrefix;\n\t@Override\n\tpublic boolean isKeyPrefix() {\n\t\treturn keyPrefix;\n\t}\n\t@Override\n\tpublic AbstractSQLConfig<T, M, L> setKeyPrefix(boolean keyPrefix) {\n\t\tthis.keyPrefix = keyPrefix;\n\t\treturn this;\n\t}\n\n\n\tpublic String gainJoinString() throws Exception {\n\t\tString joinOns = \"\";\n\n\t\tif (joinList != null) {\n\t\t\tString quote = getQuote();\n\t\t\tList<Object> pvl = getPreparedValueList();  // new ArrayList<>();\n\t\t\t//boolean changed = false;\n\n\t\t\t//  主表不用别名\t\t\tString ta;\n\t\t\tfor (Join j : joinList) {\n\t\t\t\tonGainJoinString(j);\n\n\t\t\t\tif (j.isAppJoin()) { // APP JOIN，只是作为一个标记，执行完主表的查询后自动执行副表的查询 User.id IN($commentIdList)\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tString type = j.getJoinType();\n\n\t\t\t\t//LEFT JOIN sys.apijson_user AS User ON User.id = Moment.userId， 都是用 = ，通过relateType处理缓存\n\t\t\t\t// <\"INNER JOIN User ON User.id = Moment.userId\", UserConfig>  TODO  AS 放 getSQLTable 内\n\t\t\t\tSQLConfig<T, M, L> jc = j.getJoinConfig();\n\t\t\t\tjc.setPrepared(isPrepared());\n\t\t\t\t// 将关联表所属数据源配置为主表数据源\n\t\t\t\tjc.setDatasource(this.getDatasource());\n\t\t\t\tString jt = jc.gainSQLAlias();\n\t\t\t\tList<On> onList = j.getOnList();\n\n\t\t\t\t//如果要强制小写，则可在子类重写这个方法再 toLowerCase\n\t\t\t\t//\t\t\t\tif (DATABASE_POSTGRESQL.equals(getDatabase())) {\n\t\t\t\t//\t\t\t\t\tjt = jt.toLowerCase();\n\t\t\t\t//\t\t\t\t\ttn = tn.toLowerCase();\n\t\t\t\t//\t\t\t\t}\n\n\t\t\t\tString sql;\n\n\t\t\t\tswitch (type) {\n\t\t\t\t//前面已跳过\t\t\t\tcase \"@\": // APP JOIN\n\t\t\t\t//\t\t\t\t\tcontinue;\n\n\t\t\t\tcase \"*\": // CROSS JOIN\n\t\t\t\t\tonGainCrossJoinString(j);\n\t\t\t\tcase \"<\": // LEFT JOIN\n\t\t\t\tcase \">\": // RIGHT JOIN\n\t\t\t\t\tjc.setMain(true).setKeyPrefix(false);\n\t\t\t\t\tsql = ( \"<\".equals(type) ? \" LEFT\" : (\">\".equals(type) ? \" RIGHT\" : \" CROSS\") )\n\t\t\t\t\t\t\t+ \" JOIN ( \" + jc.gainSQL(isPrepared()) + \" ) \" + gainAs() + quote + jt + quote;\n\t\t\t\t\tsql = concatJoinOn(sql, quote, j, jt, onList);\n\n\t\t\t\t\tjc.setMain(false).setKeyPrefix(true);\n\n\t\t\t\t\tpvl.addAll(jc.getPreparedValueList());\n\t\t\t\t\t//changed = true;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"&\": // INNER JOIN: A & B\n\t\t\t\tcase \"\":  // FULL JOIN: A | B\n\t\t\t\tcase \"|\": // FULL JOIN: A | B\n\t\t\t\tcase \"!\": // OUTER JOIN: ! (A | B)\n\t\t\t\tcase \"^\": // SIDE JOIN: ! (A & B)\n\t\t\t\tcase \"(\": // ANTI JOIN: A & ! B\n\t\t\t\tcase \")\": // FOREIGN JOIN: B & ! A\n\t\t\t\t\tsql = \" INNER JOIN \" + jc.gainTablePath();\n\t\t\t\t\tsql = concatJoinOn(sql, quote, j, jt, onList);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"~\": // ASOF JOIN: B ~= A\n\t\t\t\t\tsql = \" ASOF JOIN \" + jc.gainTablePath();\n\t\t\t\t\tsql = concatJoinOn(sql, quote, j, jt, onList);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tString k = jc.gainTableKey();\n\t\t\t\t\tthrow new UnsupportedOperationException(\n\t\t\t\t\t\t\t\"join:value 中 value 里的 \" + k + \"/\" + j.getPath()\n\t\t\t\t\t\t\t+ \"错误！不支持 \" + k + \" 等 [ @ APP, < LEFT, > RIGHT, * CROSS\"\n\t\t\t\t\t\t\t+ \", & INNER, | FULL, ! OUTER, ^ SIDE, ( ANTI, ) FOREIGN, ~ ASOF ] 之外的 JOIN 类型 !\"\n\t\t\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tSQLConfig<T, M, L> oc = j.getOnConfig();\n\t\t\t\tString ow = null;\n\t\t\t\tif (oc != null) {\n\t\t\t\t\toc.setPrepared(isPrepared());\n\t\t\t\t\toc.setPreparedValueList(new ArrayList<>());\n\t\t\t\t\toc.setMain(false).setKeyPrefix(true);\n\t\t\t\t\tow = oc.gainWhereString(false);\n\n\t\t\t\t\tpvl.addAll(oc.getPreparedValueList());\n\t\t\t\t\t//changed = true;\n\t\t\t\t}\n\n\t\t\t\tjoinOns += \"  \\n  \" + sql + (StringUtil.isEmpty(ow, true) ? \"\" : \" AND ( \" + ow + \" ) \");\n\t\t\t}\n\n\n\t\t\t//if (changed) {\n\t\t\t//  List<Object> opvl = getPreparedValueList();\n\t\t\t//  if (opvl != null && opvl.isEmpty() == false) {\n\t\t\t//    pvl.addAll(opvl);\n\t\t\t//  }\n\t\t\t\tsetPreparedValueList(pvl);\n\t\t\t//}\n\n\t\t}\n\n\t\treturn StringUtil.isEmpty(joinOns, true) ? \"\" : joinOns + \"  \\n\";\n\t}\n\n\tprotected String concatJoinOn(@NotNull String sql, @NotNull String quote, @NotNull Join<T, M, L> join, @NotNull String jt, List<On> onList) {\n\t\tif (onList != null) {\n\t\t\tSQLConfig<T, M, L> jc = join.getJoinConfig();\n\t\t\tMap<String, String> castMap = jc == null ? null : jc.getCast();\n\n\t\t\tboolean first = true;\n\t\t\tfor (On on : onList) {\n\t\t\t\tLogic logic = on.getLogic();\n\t\t\t\tboolean isNot = logic == null ? false : logic.isNot();\n\t\t\t\tif (isNot) {\n\t\t\t\t\tonJoinNotRelation(sql, quote, join, jt, onList, on);\n\t\t\t\t}\n\n\t\t\t\tString lk = quote + jt + quote + \".\" + quote + on.getKey() + quote;\n\t\t\t\tObject ct = castMap == null ? null : castMap.get(on.getOriginKey());\n\t\t\t\tif (StringUtil.isNotEmpty(ct, false)) {\n\t\t\t\t\tlk = \"cast(\" + lk + \" AS \" + ct + \")\"; // 解决 JOIN ON 不支持 @cast 问题，CAST(expression AS TYPE) 中 AS 不能省略\n\t\t\t\t}\n\n\t\t\t\tString rt = on.getRelateType();\n\n\t\t\t\tString rk = quote + SQLConfig.gainSQLAlias(on.getTargetTable(), on.getTargetAlias()) + quote + \".\" + quote + on.getTargetKey() + quote;\n\n\t\t\t\tif (StringUtil.isEmpty(rt, false)) {\n\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? \" != \" : \" = \") + rk;\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tonJoinComplexRelation(sql, quote, join, jt, onList, on);\n\n\t\t\t\t\tif (\">=\".equals(rt) || \"<=\".equals(rt) || \">\".equals(rt) || \"<\".equals(rt)) {\n\t\t\t\t\t\tif (isNot) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"join:value 中 value 里的 \" + jt + \"/\" + join.getPath()\n\t\t\t\t\t\t\t+ \" 中 JOIN ON 条件关联逻辑符 \" + rt + \" 不合法！ >, <, >=, <= 不支持与或非逻辑符 & | ! ！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsql += (first ? ON : AND) + lk + \" \" + rt + \" \" + rk;\n\t\t\t\t\t}\n\t\t\t\t\telse if (rt.endsWith(\"$\")) {\n\t\t\t\t\t\tString t = rt.substring(0, rt.length() - 1);\n\t\t\t\t\t\tchar r = t.isEmpty() ? 0 : t.charAt(t.length() - 1);\n\n\t\t\t\t\t\tchar l;\n\t\t\t\t\t\tif (r == '%' || r == '_' || r == '?') {\n\t\t\t\t\t\t\tt = t.substring(0, t.length() - 1);\n\n\t\t\t\t\t\t\tif (t.isEmpty()) {\n\t\t\t\t\t\t\t\tif (r == '?') {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(on.getOriginKey() + \":value 中字符 \" + on.getOriginKey()\n\t\t\t\t\t\t\t\t\t\t\t+ \" 不合法！key$:value 中不允许只有单独的 '?'，必须和 '%', '_' 之一配合使用 ！\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tl = r;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tl = t.charAt(t.length() - 1);\n\t\t\t\t\t\t\t\tif (l == '%' || l == '_' || l == '?') {\n\t\t\t\t\t\t\t\t\tif (l == r) {\n\t\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(on.getOriginKey()\n\t\t\t\t\t\t\t\t\t\t\t\t+ \":value 中字符 \" + t + \" 不合法！key$:value 中不允许 key 中有连续相同的占位符！\");\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\tt = t.substring(0, t.length() - 1);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\telse if (l > 0 && StringUtil.isName(String.valueOf(l))) {\n\t\t\t\t\t\t\t\t\tl = r;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (l == '?') {\n\t\t\t\t\t\t\t\tl = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (r == '?') {\n\t\t\t\t\t\t\t\tr = 0;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tl = r = 0;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (l <= 0 && r <= 0) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? NOT : \"\") + \" LIKE \" + rk;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? NOT : \"\")\n\t\t\t\t\t\t\t\t\t+ (l <= 0 ? \" LIKE concat(\" : \" LIKE concat('\" + l + \"', \") + rk + (r <= 0 ? \")\" : \", '\" + r + \"')\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (rt.endsWith(\"~\")) {\n\t\t\t\t\t\tboolean ignoreCase = \"*~\".equals(rt);\n\t\t\t\t\t\tif (isPSQL()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? NOT : \"\") + \" ~\" + (ignoreCase ? \"* \" : \" \") + rk;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isOracle() || isDameng() || isKingBase()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + \"regexp_like(\" +  lk + \", \" + rk + (ignoreCase ? \", 'i'\" : \", 'c'\") + \")\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isPresto() || isTrino()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + \"regexp_like(\" + (ignoreCase ? \"lower(\" : \"\") + lk + (ignoreCase ? \")\" : \"\")\n\t\t\t\t\t\t\t\t\t+ \", \" + (ignoreCase ? \"lower(\" : \"\") + rk + (ignoreCase ? \")\" : \"\") + \")\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isClickHouse()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + \"match(\" + (ignoreCase ? \"lower(\" : \"\") + lk + (ignoreCase ? \")\" : \"\")\n\t\t\t\t\t\t\t\t\t+ \", \" + (ignoreCase ? \"lower(\" : \"\") + rk + (ignoreCase ? \")\" : \"\") + \")\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isElasticsearch()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? NOT : \"\") + \" RLIKE \" + rk;\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isHive()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (ignoreCase ? \"lower(\" : \"\") + lk + (ignoreCase ? \")\" : \"\")\n\t\t\t\t\t\t\t\t\t+ \" REGEXP \" + (ignoreCase ? \"lower(\" : \"\") + rk + (ignoreCase ? \")\" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + lk + (isNot ? NOT : \"\") + \" REGEXP \" + (ignoreCase ? \"\" : \"BINARY \") + rk;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (\"{}\".equals(rt) || \"<>\".equals(rt)) {\n\t\t\t\t\t\tString tt = on.getTargetTable();\n\t\t\t\t\t\tString ta = on.getTargetAlias();\n\n\t\t\t\t\t\tMap<String, String> cast = null;\n\t\t\t\t\t\tif (tt.equals(getTable()) && Objects.equals(ta, getAlias())) {\n\t\t\t\t\t\t\tcast = getCast();\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tboolean find = false;\n\t\t\t\t\t\t\tfor (Join<T, M, L> jn : joinList) {\n\t\t\t\t\t\t\t\tif (tt.equals(jn.getTable()) && Objects.equals(ta, jn.getAlias())) {\n\t\t\t\t\t\t\t\t\tcast = getCast();\n\t\t\t\t\t\t\t\t\tfind = true;\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (find == false) {\n\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(\"join:value 中 value 里的 \" + jt + \"/\" + join.getPath()\n\t\t\t\t\t\t\t\t+ \" 中 JOIN ON 条件中找不到对应的 \" + rt + \" 不合法！只支持 =, {}, <> 这几种！\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tboolean isBoolOrNum = SQL.isBooleanOrNumber(cast == null ? null : cast.get(on.getTargetKey()));\n\n\t\t\t\t\t\tboolean isIn = \"{}\".equals(rt);\n\t\t\t\t\t\tString arrKeyPath = isIn ? rk : lk;\n\t\t\t\t\t\tString itemKeyPath = isIn ? lk : rk;\n\n\t\t\t\t\t\tif (isPSQL()) {  //operator does not exist: jsonb @> character varying  \"[\" + c + \"]\");\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (isNot ? \"( \" : \"\") + gainCondition(isNot, arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" IS NOT NULL AND \" + arrKeyPath + \" @> \" + itemKeyPath) + (isNot ? \") \" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isOracle() || isDameng() || isKingBase()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (isNot ? \"( \" : \"\") + gainCondition(isNot, arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" IS NOT NULL AND json_textcontains(\" + arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \", '$', \" + itemKeyPath + \")\") + (isNot ? \") \" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isPresto() || isTrino()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (isNot ? \"( \" : \"\") + gainCondition(isNot, arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" IS NOT NULL AND json_array_contains(cast(\" + arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" AS VARCHAR), \" + itemKeyPath + \")\") + (isNot ? \") \" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse if (isClickHouse()) {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (isNot ? \"( \" : \"\") + gainCondition(isNot, arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" IS NOT NULL AND has(JSONExtractArrayRaw(assumeNotNull(\" + arrKeyPath + \"))\"\n\t\t\t\t\t\t\t\t\t+ \", \" + itemKeyPath + \")\") + (isNot ? \") \" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse {\n\t\t\t\t\t\t\tsql += (first ? ON : AND) + (isNot ? \"( \" : \"\") + gainCondition(isNot, arrKeyPath\n\t\t\t\t\t\t\t\t\t+ \" IS NOT NULL AND json_contains(\" + arrKeyPath\n\t\t\t\t\t\t\t\t\t+ (isBoolOrNum ? \", cast(\" + itemKeyPath + \" AS CHAR), '$')\"\n\t\t\t\t\t\t\t\t\t\t\t: \", concat('\\\"', \" + itemKeyPath + \", '\\\"'), '$')\"\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t) + (isNot ? \") \" : \"\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"join:value 中 value 里的 \" + jt + \"/\" + join.getPath()\n\t\t\t\t\t\t+ \" 中 JOIN ON 条件关联类型 \" + rt + \" 不合法！只支持 =, >, <, >=, <=, !=, $, ~, {}, <> 这几种！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfirst = false;\n\t\t\t}\n\t\t}\n\n\t\treturn sql;\n\t}\n\n\tprotected void onJoinNotRelation(String sql, String quote, Join<T, M, L> join, String table, List<On> onList, On on) {\n\t\tthrow new UnsupportedOperationException(\"JOIN 已禁用 '!' 非逻辑连接符 ！性能很差、需求极少，如要取消禁用可在后端重写相关方法！\");\n\t}\n\tprotected void onJoinComplexRelation(String sql, String quote, Join<T, M, L> join, String table, List<On> onList, On on) {\n\t\tthrow new UnsupportedOperationException(\"JOIN 已禁用 $, ~, {}, <>, >, <, >=, <= 等复杂关联 ！\" +\n\t\t\t\t\"性能很差、需求极少，默认只允许 = 等价关联，如要取消禁用可在后端重写相关方法！\");\n\t}\n\n\tprotected void onGainJoinString(Join<T, M, L> join) throws UnsupportedOperationException {\n\t}\n\tprotected void onGainCrossJoinString(Join<T, M, L> join) throws UnsupportedOperationException {\n\t\tthrow new UnsupportedOperationException(\"已禁用 * CROSS JOIN ！性能很差、需求极少，如要取消禁用可在后端重写相关方法！\");\n\t}\n\n\t/**新建SQL配置\n\t * @param table\n\t * @param request\n\t * @param joinList\n\t * @param isProcedure\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> SQLConfig<T, M, L> newSQLConfig(\n\t\t\tRequestMethod method, String table, String alias\n\t\t\t, M request, List<Join<T, M, L>> joinList, boolean isProcedure, Callback<T, M, L> callback) throws Exception {\n\t\tif (request == null) { // User:{} 这种空内容在查询时也有效\n\t\t\tthrow new NullPointerException(TAG + \": newSQLConfig<T, M, L>  request == null!\");\n\t\t}\n\n\t\tBoolean explain = getBoolean(request, KEY_EXPLAIN);\n\t\tif (explain != null && explain && Log.DEBUG == false) { // 不在 config.setExplain 抛异常，一方面处理更早性能更好，另一方面为了内部调用可以绕过这个限制\n\t\t\tthrow new UnsupportedOperationException(\"非DEBUG模式, 不允许传 \" + KEY_EXPLAIN + \" ！\");\n\t\t}\n\n\t\tString database = getString(request, KEY_DATABASE);\n\t\tif (StringUtil.isNotEmpty(database, false) && DATABASE_LIST.contains(database) == false) {\n\t\t\tthrow new UnsupportedDataTypeException(\"@database:value 中 value 错误，只能是 [\"\n\t\t\t\t\t+ StringUtil.get(DATABASE_LIST.toArray()) + \"] 中的一种！\");\n\t\t}\n\n\t\tString datasource = getString(request, KEY_DATASOURCE);\n\t\tString namespace = getString(request, KEY_NAMESPACE);\n\t\tString catalog = getString(request, KEY_CATALOG);\n\t\tString schema = getString(request, KEY_SCHEMA);\n\n\t\tSQLConfig<T, M, L> config = (SQLConfig<T, M, L>) callback.getSQLConfig(method, database, schema, datasource, table);\n\t\tconfig.setAlias(alias);\n\n\t\tconfig.setDatabase(database); // 不删，后面表对象还要用的，必须放在 parseJoin 前\n\t\tconfig.setDatasource(datasource); // 不删，后面表对象还要用的\n\t\tconfig.setNamespace(namespace); // 不删，后面表对象还要用的\n\t\tconfig.setCatalog(catalog); // 不删，后面表对象还要用的\n\t\tconfig.setSchema(schema); // 不删，后面表对象还要用的\n\n\t\tif (isProcedure) {\n\t\t\treturn config;\n\t\t}\n\n\t\tconfig = parseJoin(method, config, joinList, callback); // 放后面会导致主表是空对象时 joinList 未解析\n\n\t\tif (request.isEmpty()) { // User:{} 这种空内容在查询时也有效\n\t\t\treturn config; // request.remove(key); 前都可以直接return，之后必须保证 put 回去\n\t\t}\n\n\t\t// 对 id, id{}, userId, userId{} 处理，这些只要不为 null 就一定会作为 AND 条件 <<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t\tString idKey = callback.getIdKey(datasource, database, schema, table);\n\t\tString idInKey = idKey + \"{}\";\n\t\tString userIdKey = callback.getUserIdKey(datasource, database, schema, table);\n\t\tString userIdInKey = userIdKey + \"{}\";\n\n\t\tObject idIn = request.get(idInKey); // 可能是 id{}:\">0\"\n\t\tif (idIn instanceof Collection) { // 排除掉 0, 负数, 空字符串 等无效 id 值\n\t\t\tCollection<?> ids = (Collection<?>) idIn;\n\t\t\tList<Object> newIdIn = new ArrayList<>();\n\t\t\tfor (Object d : ids) { // 不用 idIn.contains(id) 因为 idIn 里存到很可能是 Integer，id 又是 Long！\n\t\t\t\tif ((d instanceof Number && ((Number) d).longValue() > 0) || (d instanceof String && StringUtil.isNotEmpty(d, true))) {\n\t\t\t\t\tif (newIdIn.contains(d) == false) {\n\t\t\t\t\t\tnewIdIn.add(d);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (newIdIn.isEmpty()) {\n\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> idIn instanceof List >> 去掉无效 id 后 newIdIn.isEmpty()\");\n\t\t\t}\n\t\t\tidIn = newIdIn;\n\n\t\t\tif (method == DELETE || method == PUT) {\n\t\t\t\tconfig.setCount(newIdIn.size());\n\t\t\t}\n\t\t}\n\n\t\tObject id = request.get(idKey);\n\t\tif (id == null && method == POST) {\n\t\t\tid = callback.newId(method, database, schema, datasource, table); // null 表示数据库自增 id\n\t\t}\n\n\t\tif (id != null) { // null 无效\n\t\t\tif (id instanceof Number) {\n\t\t\t\tif (((Number) id).longValue() <= 0) { // 一定没有值\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> \" + table + \".id <= 0\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (id instanceof String) {\n\t\t\t\tif (StringUtil.isEmpty(id, true)) { // 一定没有值\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> StringUtil.isEmpty(\" + table + \".id, true)\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (id instanceof Subquery) {}\n\t\t\telse {\n\t\t\t\tthrow new IllegalArgumentException(idKey + \":value 中 value 的类型只能是 Long , String 或 Subquery ！\");\n\t\t\t}\n\n\t\t\tif (idIn instanceof Collection) { // 共用idIn场景少性能差\n\t\t\t\tboolean contains = false;\n\t\t\t\tCollection<?> idList = ((Collection<?>) idIn);\n\t\t\t\tfor (Object d : idList) { // 不用 idIn.contains(id) 因为 idIn 里存到很可能是 Integer，id 又是 Long！\n\t\t\t\t\tif (d != null && id.toString().equals(d.toString())) {\n\t\t\t\t\t\tcontains = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (contains == false) { // empty有效  BaseModel.isEmpty(idIn) == false) {\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L>  idIn != null && (((List<?>) idIn).contains(id) == false\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (method == DELETE || method == PUT) {\n\t\t\t\tconfig.setCount(1);\n\t\t\t}\n\t\t}\n\n\t\t// 对 id, id{}, userId, userId{} 处理，这些只要不为 null 就一定会作为 AND 条件 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tObject userIdIn = userIdInKey.equals(idInKey) ? null : request.get(userIdInKey); // 可能是 userId{}:\">0\"\n\t\tif (userIdIn instanceof Collection) { // 排除掉 0, 负数, 空字符串 等无效 userId 值\n\t\t\tCollection<?> userIds = (Collection<?>) userIdIn;\n\t\t\tList<Object> newUserIdIn = new ArrayList<>();\n\t\t\tfor (Object d : userIds) { // 不用 userIdIn.contains(userId) 因为 userIdIn 里存到很可能是 Integer，userId 又是 Long！\n\t\t\t\tif ((d instanceof Number && ((Number) d).longValue() > 0) || (d instanceof String && StringUtil.isNotEmpty(d, true))) {\n\t\t\t\t\tif (newUserIdIn.contains(d) == false) {\n\t\t\t\t\t\tnewUserIdIn.add(d);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (newUserIdIn.isEmpty()) {\n\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> userIdIn instanceof List >> 去掉无效 userId 后 newIdIn.isEmpty()\");\n\t\t\t}\n\t\t\tuserIdIn = newUserIdIn;\n\t\t}\n\n\t\tObject userId = userIdKey.equals(idKey) ? null : request.get(userIdKey);\n\t\tif (userId != null) { // null 无效\n\t\t\tif (userId instanceof Number) {\n\t\t\t\tif (((Number) userId).longValue() <= 0) { // 一定没有值\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> \" + table + \".userId <= 0\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (userId instanceof String) {\n\t\t\t\tif (StringUtil.isEmpty(userId, true)) { // 一定没有值\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L> StringUtil.isEmpty(\" + table + \".userId, true)\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (userId instanceof Subquery) {}\n\t\t\telse {\n\t\t\t\tthrow new IllegalArgumentException(userIdKey + \":value 中 value 的类型只能是 Long , String 或 Subquery ！\");\n\t\t\t}\n\n\t\t\tif (userIdIn instanceof Collection) { // 共用 userIdIn 场景少性能差\n\t\t\t\tboolean contains = false;\n\t\t\t\tCollection<?> userIds = (Collection<?>) userIdIn;\n\t\t\t\tfor (Object d : userIds) { // 不用 userIdIn.contains(userId) 因为 userIdIn 里存到很可能是 Integer，userId 又是 Long！\n\t\t\t\t\tif (d != null && userId.toString().equals(d.toString())) {\n\t\t\t\t\t\tcontains = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (contains == false) { // empty有效  BaseModel.isEmpty(userIdIn) == false) {\n\t\t\t\t\tthrow new NotExistException(TAG + \": newSQLConfig<T, M, L>  userIdIn != null && (((List<?>) userIdIn).contains(userId) == false\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// 对 id, id{}, userId, userId{} 处理，这些只要不为 null 就一定会作为 AND 条件 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t\tString role = getString(request, KEY_ROLE);\n\t\tString cache = getString(request, KEY_CACHE);\n\t\tSubquery<T, M, L> from = (Subquery<T, M, L>) request.get(KEY_FROM);\n\t\tString column = getString(request, KEY_COLUMN);\n\t\tString nulls = getString(request, KEY_NULL);\n\t\tString cast = getString(request, KEY_CAST);\n\t\tString combine = getString(request, KEY_COMBINE);\n\t\tString group = getString(request, KEY_GROUP);\n\t\tObject having = request.get(KEY_HAVING);\n\t\tString havingAnd = getString(request, KEY_HAVING_AND);\n\t\tString sample = getString(request, KEY_SAMPLE);\n\t\tString latest = getString(request, KEY_LATEST);\n\t\tString partition = getString(request, KEY_PARTITION);\n\t\tString fill = getString(request, KEY_FILL);\n\t\tString order = getString(request, KEY_ORDER);\n\t\tObject keyMap = request.get(KEY_KEY);\n\t\tString raw = getString(request, KEY_RAW);\n\t\tString json = getString(request, KEY_JSON);\n\t\tString mthd = getString(request, KEY_METHOD);\n\n\t\ttry {\n\t\t\t// 强制作为条件且放在最前面优化性能\n\t\t\trequest.remove(idKey);\n\t\t\trequest.remove(idInKey);\n\t\t\trequest.remove(userIdKey);\n\t\t\trequest.remove(userIdInKey);\n\t\t\t// 关键词\n\t\t\trequest.remove(KEY_ROLE);\n\t\t\trequest.remove(KEY_EXPLAIN);\n\t\t\trequest.remove(KEY_CACHE);\n\t\t\trequest.remove(KEY_DATABASE);\n\t\t\trequest.remove(KEY_DATASOURCE);\n\t\t\trequest.remove(KEY_NAMESPACE);\n\t\t\trequest.remove(KEY_CATALOG);\n\t\t\trequest.remove(KEY_SCHEMA);\n\t\t\trequest.remove(KEY_FROM);\n\t\t\trequest.remove(KEY_COLUMN);\n\t\t\trequest.remove(KEY_NULL);\n\t\t\trequest.remove(KEY_CAST);\n\t\t\trequest.remove(KEY_COMBINE);\n\t\t\trequest.remove(KEY_GROUP);\n\t\t\trequest.remove(KEY_HAVING);\n\t\t\trequest.remove(KEY_HAVING_AND);\n\t\t\trequest.remove(KEY_SAMPLE);\n\t\t\trequest.remove(KEY_LATEST);\n\t\t\trequest.remove(KEY_PARTITION);\n\t\t\trequest.remove(KEY_FILL);\n\t\t\trequest.remove(KEY_ORDER);\n\t\t\trequest.remove(KEY_KEY);\n\t\t\trequest.remove(KEY_RAW);\n\t\t\trequest.remove(KEY_JSON);\n\t\t\trequest.remove(KEY_METHOD);\n\n\n\t\t\t// @null <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\t\tString[] nullKeys = StringUtil.split(nulls);\n\t\t\tif (nullKeys != null && nullKeys.length > 0) {\n\t\t\t\tfor (String nk : nullKeys) {\n\t\t\t\t\tif (StringUtil.isEmpty(nk, true)) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ @null: value } 中的字符 '\" + nk + \"' 不合法！不允许为空！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (request.get(nk) != null) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ @null: value } 中的字符 '\"\n\t\t\t\t\t\t\t\t+ nk + \"' 已在当前对象有非 null 值！不允许对同一个 JSON key 设置不同值！\");\n\t\t\t\t\t}\n\n\t\t\t\t\trequest.put(nk, null);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// @null >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t\t\t// @cast <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\t\tString[] casts = StringUtil.split(cast);\n\t\t\tMap<String, String> castMap = null;\n\t\t\tif (casts != null && casts.length > 0) {\n\t\t\t\tcastMap = new HashMap<>(casts.length);\n\t\t\t\tfor (String c : casts) {\n\t\t\t\t\tapijson.orm.Entry<String, String> p = Pair.parseEntry(c);\n\n\t\t\t\t\tif (StringUtil.isEmpty(p.getKey(), true)) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @cast: 'key0:type0,key1:type1..' 中 '\"\n\t\t\t\t\t\t\t\t+ c +  \"' 对应的 key 的字符 '\" + p.getKey() + \"' 不合法！不允许为空！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (StringUtil.isName(p.getValue()) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @cast: 'key0:type0,key1:type1..' 中 '\"\n\t\t\t\t\t\t\t\t+ c +  \"' 对应的 type 的字符 '\" + p.getValue() + \"' 不合法！必须符合类型名称格式！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (castMap.get(p.getKey()) != null) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @cast: 'key0:type0,key1:type1..' 中 '\"\n\t\t\t\t\t\t\t\t+ c +  \"' 对应的 key 的字符 '\" + p.getKey() + \"' 已存在！不允许重复设置类型！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tcastMap.put(p.getKey(), p.getValue());\n\t\t\t\t}\n\t\t\t}\n\t\t\t// @cast >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t\t\tString[] rawArr = StringUtil.split(raw);\n\t\t\tconfig.setRaw(rawArr == null || rawArr.length <= 0 ? null : new ArrayList<>(Arrays.asList(rawArr)));\n\n\t\t\tMap<String, Object> tableWhere = new LinkedHashMap<String, Object>(); // 保证顺序好优化 WHERE id > 1 AND name LIKE...\n\n\t\t\tboolean ignoreBlankStr = IGNORE_BLANK_STRING_METHOD_LIST != null && IGNORE_BLANK_STRING_METHOD_LIST.contains(method);\n\t\t\tboolean ignoreEmptyStr = ignoreBlankStr || (IGNORE_EMPTY_STRING_METHOD_LIST != null && IGNORE_EMPTY_STRING_METHOD_LIST.contains(method));\n\t\t\tboolean ignoreEmptyOrBlankStr = ignoreEmptyStr || ignoreBlankStr;\n\n\t\t\tboolean enableFakeDelete = config.isFakeDelete();\n\n\t\t\t// 已经 remove了 id 和 id{}，以及 @key\n\t\t\tSet<String> set = request.keySet(); // 前面已经判断 request 是否为空\n\t\t\tif (method == POST) { // POST操作\n\t\t\t\tif (idIn != null) {\n\t\t\t\t\tthrow new IllegalArgumentException(table + \":{\" + idInKey + \": value} 里的 key 不合法！POST 请求中不允许传 \" + idInKey\n\t\t\t\t\t\t\t+ \" 这种非字段命名 key ！必须为 英文字母 开头且只包含 英文字母、数字、下划线的 字段命名！\");\t\t\t\t}\n\t\t\t\tif (userIdIn != null) {\n\t\t\t\t\tthrow new IllegalArgumentException(table + \":{\" + userIdInKey + \": value} 里的 key 不合法！POST 请求中不允许传 \" + userIdInKey\n\t\t\t\t\t\t\t+ \" 这种非字段命名 key ！必须为 英文字母 开头且只包含 英文字母、数字、下划线的 字段命名！\");\t\t\t\t}\n\n\t\t\t\tif (set != null && set.isEmpty() == false) { // 不能直接return，要走完下面的流程\n\t\t\t\t\tfor (String k : set) {\n\t\t\t\t\t\tif (StringUtil.isName(k) == false) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{\" + k + \": value} 里的 key 不合法！POST 请求中不允许传 \" + k\n\t\t\t\t\t\t\t\t\t+ \" 这种非字段命名 key ！必须为 英文字母 开头且只包含 英文字母、数字、下划线的 字段命名！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tString[] columns = set.toArray(new String[]{});\n\n\t\t\t\t\tCollection<Object> valueCollection = request.values();\n\t\t\t\t\tObject[] values = valueCollection == null ? null : valueCollection.toArray();\n\n\t\t\t\t\tif (values == null || values.length != columns.length) {\n\t\t\t\t\t\tthrow new Exception(\"服务器内部错误:\\n\" + TAG\n\t\t\t\t\t\t\t\t+ \" newSQLConfig<T, M, L>  values == null || values.length != columns.length !\");\n\t\t\t\t\t}\n\n\t\t\t\t\tcolumn = (id == null ? \"\" : idKey + \",\") + (userId == null ? \"\" : userIdKey + \",\")\n\t\t\t\t\t\t\t+ StringUtil.get(columns); //set已经判断过不为空\n\n\t\t\t\t\tint idCount = id == null ? (userId == null ? 0 : 1) : (userId == null ? 1 : 2);\n\t\t\t\t\tint size = idCount + columns.length; // 以 key 数量为准\n\n\t\t\t\t\tList<Object> items = new ArrayList<>(size);  // VALUES(item0, item1, ...)\n\t\t\t\t\tif (id != null) {\n\t\t\t\t\t\titems.add(id); // idList.get(i)); // 第 0 个就是 id\n\t\t\t\t\t}\n\t\t\t\t\tif (userId != null) {\n\t\t\t\t\t\titems.add(userId); // idList.get(i)); // 第 1 个就是 userId\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (int j = 0; j < values.length; j++) {\n\t\t\t\t\t\titems.add(values[j]); // 从第 1 个开始，允许 \"null\"\n\t\t\t\t\t}\n\n\t\t\t\t\tList<List<Object>> valuess = new ArrayList<>(1);\n\t\t\t\t\tvaluess.add(items);\n\t\t\t\t\tconfig.setValues(valuess);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse { // 非 POST 操作\n\t\t\t\tfinal boolean isWhere = method != PUT; // 除了POST,PUT，其它全是条件！！！\n\n\t\t\t\t// 条件<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\t\t\tString[] ws = StringUtil.split(combine);\n\t\t\t\tString combineExpr = ws == null || ws.length != 1 ? null : ws[0];\n\n\t\t\t\tMap<String, List<String>> combineMap = new LinkedHashMap<>();\n\t\t\t\tList<String> andList = new ArrayList<>();\n\t\t\t\tList<String> orList = new ArrayList<>();\n\t\t\t\tList<String> notList = new ArrayList<>();\n\n\t\t\t\tList<String> whereList = new ArrayList<>();\n\n\t\t\t\t// 强制作为条件且放在最前面优化性能\n\t\t\t\tif (id != null) {\n\t\t\t\t\ttableWhere.put(idKey, id);\n\t\t\t\t\tandList.add(idKey);\n\t\t\t\t\twhereList.add(idKey);\n\t\t\t\t}\n\t\t\t\tif (idIn != null) {\n\t\t\t\t\ttableWhere.put(idInKey, idIn);\n\t\t\t\t\tandList.add(idInKey);\n\t\t\t\t\twhereList.add(idInKey);\n\t\t\t\t}\n\t\t\t\tif (userId != null) {\n\t\t\t\t\ttableWhere.put(userIdKey, userId);\n\t\t\t\t\tandList.add(userIdKey);\n\t\t\t\t\twhereList.add(userIdKey);\n\t\t\t\t}\n\t\t\t\tif (userIdIn != null) {\n\t\t\t\t\ttableWhere.put(userIdInKey, userIdIn);\n\t\t\t\t\tandList.add(userIdInKey);\n\t\t\t\t\twhereList.add(userIdInKey);\n\t\t\t\t}\n\n\t\t\t\tif (enableFakeDelete) {\n\t\t\t\t\t// 查询 Access 假删除\n\t\t\t\t\tMap<String, Object> accessFakeDeleteMap = method == DELETE\n\t\t\t\t\t\t\t? null : AbstractVerifier.ACCESS_FAKE_DELETE_MAP.get(config.getTable());\n\t\t\t\t\tObject deletedKey = accessFakeDeleteMap == null ? null : accessFakeDeleteMap.get(KEY_DELETED_KEY);\n\t\t\t\t\tboolean hasKey = deletedKey instanceof String && StringUtil.isNotEmpty(deletedKey, true);\n\t\t\t\t\tObject deletedValue = hasKey ? accessFakeDeleteMap.get(KEY_DELETED_VALUE) : null;\n\t\t\t\t\tboolean containNotDeletedValue = hasKey && accessFakeDeleteMap.containsKey(KEY_NOT_DELETED_VALUE);\n\t\t\t\t\tObject notDeletedValue = containNotDeletedValue ? accessFakeDeleteMap.get(KEY_NOT_DELETED_VALUE) : null;\n\n\t\t\t\t\tif (deletedValue != null || containNotDeletedValue) {\n\t\t\t\t\t\tboolean isFakeDelete = true;\n\t\t\t\t\t\tif (from != null) {\n\t\t\t\t\t\t\t// 兼容 JOIN 外层 SELECT 重复生成 deletedKey\n\t\t\t\t\t\t\tSQLConfig<?, ?, ?> cfg = from.gainConfig();\n\t\t\t\t\t\t\tif (cfg != null && StringUtil.equals(table, cfg.getTable())) {\n\t\t\t\t\t\t\t\tisFakeDelete = false;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tList<? extends Join<?, ?, ?>> jl = isFakeDelete && cfg != null ? cfg.getJoinList() : null;\n\t\t\t\t\t\t\tif (jl != null) {\n\t\t\t\t\t\t\t\tfor (Join<?, ?, ?> join : jl) {\n\t\t\t\t\t\t\t\t\tif (join != null && StringUtil.equals(table, join.getTable())) {\n\t\t\t\t\t\t\t\t\t\tisFakeDelete = false;\n\t\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (isFakeDelete) { // 支持 deleted != 1 / deleted is null 等表达式\n\t\t\t\t\t\t\tif (deletedValue != null) { // deletedKey != deletedValue\n\t\t\t\t\t\t\t\tString key = deletedKey + \"!\";\n\t\t\t\t\t\t\t\ttableWhere.put(key, deletedValue);\n\t\t\t\t\t\t\t\tandList.add(key);\n\t\t\t\t\t\t\t\twhereList.add(key);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (containNotDeletedValue) { // deletedKey = notDeletedValue\n\t\t\t\t\t\t\t\tString key = deletedKey.toString();\n\t\t\t\t\t\t\t\ttableWhere.put(key, notDeletedValue);\n\t\t\t\t\t\t\t\tandList.add(key);\n\t\t\t\t\t\t\t\twhereList.add(key);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (StringUtil.isNotEmpty(combineExpr, true)) {\n\t\t\t\t\tList<String> banKeyList = Arrays.asList(idKey, idInKey, userIdKey, userIdInKey);\n\t\t\t\t\tfor (String key : banKeyList) {\n\t\t\t\t\t\tif (isKeyInCombineExpr(combineExpr, key)) {\n\t\t\t\t\t\t\tthrow new UnsupportedOperationException(table + \":{} 里的 @combine:value 中的 value 里 \" + key + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t+ \"不允许传 [\" + idKey + \", \" + idInKey + \", \" + userIdKey + \", \" + userIdInKey + \"] 其中任何一个！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (ws != null) {\n\t\t\t\t\tfor (int i = 0; i < ws.length; i++) { // 去除 &,|,! 前缀\n\t\t\t\t\t\tString w = ws[i];\n\t\t\t\t\t\tif (w != null) {\n\t\t\t\t\t\t\tif (w.startsWith(\"&\")) {\n\t\t\t\t\t\t\t\tw = w.substring(1);\n\t\t\t\t\t\t\t\tandList.add(w);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (w.startsWith(\"|\")) {\n\t\t\t\t\t\t\t\tif (method == PUT) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @combine:value 中的value里条件 \" + ws[i] + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"PUT请求的 @combine:\\\"key0,key1,...\\\" 不允许传 |key 或 !key !\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tw = w.substring(1);\n\t\t\t\t\t\t\t\torList.add(w);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse if (w.startsWith(\"!\")) {\n\t\t\t\t\t\t\t\tif (method == PUT) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @combine:value 中的value里条件 \" + ws[i] + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"PUT请求的 @combine:\\\"key0,key1,...\\\" 不允许传 |key 或 !key !\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tw = w.substring(1);\n\t\t\t\t\t\t\t\tnotList.add(w);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\torList.add(w);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (w.isEmpty()) {\n\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{} 里的 @combine:value 中的value里条件 \" + ws[i] + \" 不合法！不允许为空值！\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tif (idKey.equals(w) || idInKey.equals(w) || userIdKey.equals(w) || userIdInKey.equals(w)) {\n\t\t\t\t\t\t\t\t\tthrow new UnsupportedOperationException(table + \":{} 里的 @combine:value 中的 value 里 \" + ws[i] + \" 不合法！\"\n\t\t\t\t\t\t\t\t\t\t\t+ \"不允许传 [\" + idKey + \", \" + idInKey + \", \" + userIdKey + \", \" + userIdInKey + \"] 其中任何一个！\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\twhereList.add(w);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// 可重写回调方法自定义处理 // 动态设置的场景似乎很少，而且去掉后不方便用户排错！\n\t\t\t\t\t\t// 去掉判断，有时候不在没关系，如果是对增删改等非开放请求强制要求传对应的条件，可以用 Operation.NECESSARY\n\t\t\t\t\t\tif (request.containsKey(w) == false) {  // 和 request.get(w) == null 没区别，前面 Parser<T, M, L> 已经过滤了 null\n\t\t\t\t\t\t\t//\tthrow new IllegalArgumentException(table + \":{} 里的 @combine:value 中的value里 \" + ws[i] + \" 对应的 \" + w + \" 不在它里面！\");\n\t\t\t\t\t\t\tcallback.onMissingKey4Combine(table, request, combine, ws[i], w);\n\t\t\t\t\t\t\tif (config instanceof AbstractSQLConfig) {\n\t\t\t\t\t\t\t\t((AbstractSQLConfig<T, M, L>) config).putWarnIfNeed(KEY_COMBINE, table + \":{} 里的 @combine:value 中的 value 里 \"\n\t\t\t\t\t\t\t\t\t\t+ ws[i] + \" 对应的条件 \" + w + \":value 中 value 必须存在且不能为 null！\");\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// 条件 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\t\t\t\tMap<String, Object> tableContent = new LinkedHashMap<String, Object>();\n\t\t\t\tfor (String key : set) {\n\t\t\t\t\tObject value = request.get(key);\n\t\t\t\t\tif (ignoreEmptyOrBlankStr && value instanceof String && StringUtil.isEmpty(value, ignoreBlankStr)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (key.endsWith(\"<>\") == false && value instanceof Map) { // 只允许常规 Object\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + key + \":value } 中 value 类型错误！除了 key<>:{} 外，不允许 \"\n\t\t\t\t\t\t\t\t+ key + \" 等其它任何 key 对应 value 的类型为 JSONMap {} !\");\n\t\t\t\t\t}\n\n\t\t\t\t\t// 兼容 PUT @combine\n\t\t\t\t\t// 解决AccessVerifier新增userId没有作为条件，而是作为内容，导致PUT，DELETE出错\n\t\t\t\t\tif ((isWhere || (StringUtil.isName(key.replaceFirst(\"[+-]$\", \"\")) == false))\n\t\t\t\t\t\t\t|| (isWhere == false && StringUtil.isNotEmpty(combineExpr, true) && isKeyInCombineExpr(combineExpr, key))) {\n\t\t\t\t\t\ttableWhere.put(key, value);\n\t\t\t\t\t\tif (whereList.contains(key) == false) {\n\t\t\t\t\t\t\tandList.add(key);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (whereList.contains(key)) {\n\t\t\t\t\t\ttableWhere.put(key, value);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttableContent.put(key, value); // 一样 instanceof List<?> ? JSON.toJSONString(value) : value);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (combineMap != null) {\n\t\t\t\t\tcombineMap.put(\"&\", andList);\n\t\t\t\t\tcombineMap.put(\"|\", orList);\n\t\t\t\t\tcombineMap.put(\"!\", notList);\n\t\t\t\t}\n\t\t\t\tconfig.setCombineMap(combineMap);\n\t\t\t\tconfig.setCombine(combineExpr);\n\n\t\t\t\tconfig.setContent(tableContent);\n\t\t\t}\n\n\t\t\tif (enableFakeDelete && method == DELETE) {\n\t\t\t\t// 查询 Access 假删除\n\t\t\t\tMap<String, Object> accessFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP.get(config.getTable());\n\n\t\t\t\tObject deletedKey = accessFakeDeleteMap.get(KEY_DELETED_KEY);\n\t\t\t\tif (StringUtil.isNotEmpty(deletedKey, true)) {\n\t\t\t\t\t// 假删除需要更新的其他字段，比如：删除时间 deletedTime 之类的\n\t\t\t\t\tMap<String, Object> fakeDeleteMap = new HashMap<>();\n\t\t\t\t\tfakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE));\n\t\t\t\t\tfakeDeleteMap = config.onFakeDelete(fakeDeleteMap);\n\n\t\t\t\t\tMap<String, Object> content = config.getContent();\n\t\t\t\t\tif (content == null || content.isEmpty()) {\n\t\t\t\t\t\tcontent = fakeDeleteMap;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontent.putAll(fakeDeleteMap);\n\t\t\t\t\t}\n\n\t\t\t\t\tconfig.setMethod(PUT);\n\t\t\t\t\tconfig.setContent(content);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tList<String> cs = new ArrayList<>();\n\n\t\t\tList<String> rawList = config.getRaw();\n\t\t\tboolean containColumnHavingAnd = rawList != null && rawList.contains(KEY_HAVING_AND);\n\n\t\t\tif (containColumnHavingAnd) {\n\t\t\t\tthrow new IllegalArgumentException(table + \":{ @raw:value } 的 value 里字符 @having& 不合法！\"\n\t\t\t\t\t\t+ \"@raw 不支持 @having&，请用 @having 替代！\");\n\t\t\t}\n\n\t\t\t// TODO 这段是否必要？如果 @column 只支持分段后的 SQL 片段，也没问题\n\t\t\tboolean containColumnRaw = rawList != null && rawList.contains(KEY_COLUMN);\n\t\t\tString rawColumnSQL = null;\n\t\t\tif (containColumnRaw) {\n\t\t\t\trawColumnSQL = config.gainRawSQL(KEY_COLUMN, column);\n\t\t\t\tif (rawColumnSQL != null) {\n\t\t\t\t\tcs.add(rawColumnSQL);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tboolean distinct = rawColumnSQL == null && column != null && column.startsWith(PREFIX_DISTINCT);\n\t\t\tif (rawColumnSQL == null) {\n\t\t\t\t// key0,key1;fun0(key0,...);fun1(key0,...);key3;fun2(key0,...)\n\t\t\t\tString[] fks = StringUtil.split(distinct ? column.substring(PREFIX_DISTINCT.length()) : column, \";\");\n\t\t\t\tif (fks != null) {\n\t\t\t\t\tfor (String fk : fks) {\n\t\t\t\t\t\tif (containColumnRaw) {\n\t\t\t\t\t\t\tString rawSQL = config.gainRawSQL(KEY_COLUMN, fk);\n\t\t\t\t\t\t\tif (rawSQL != null) {\n\t\t\t\t\t\t\t\tcs.add(rawSQL);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (fk.contains(\"(\")) {  // fun0(key0,...)\n\t\t\t\t\t\t\tcs.add(fk);\n\t\t\t\t\t\t}\n\t\t\t\t\t\telse { // key0,key1...\n\t\t\t\t\t\t\tString[] ks = StringUtil.split(fk);\n\t\t\t\t\t\t\tif (ks != null && ks.length > 0) {\n\t\t\t\t\t\t\t\tcs.addAll(Arrays.asList(ks));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\n\t\t\t// @having, @haivng& <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\t\tObject newHaving = having;\n\t\t\tboolean isHavingAnd = false;\n\n\t\t\tMap<String, Object> havingMap = new LinkedHashMap<>();\n\t\t\tif (havingAnd != null) {\n\t\t\t\tif (having != null) {\n\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ @having: value1, @having&: value2 } \"\n\t\t\t\t\t\t\t+ \"中 value1 与 value2 不合法！不允许同时传 @having 和 @having& ，两者最多传一个！\");\n\t\t\t\t}\n\n\t\t\t\tnewHaving = havingAnd;\n\t\t\t\tisHavingAnd = true;\n\t\t\t}\n\n\t\t\tString havingKey = (isHavingAnd ? KEY_HAVING_AND : KEY_HAVING);\n\t\t\tString havingCombine = \"\";\n\n\t\t\tif (newHaving instanceof String) {\n\t\t\t\tString[] havingss = StringUtil.split((String) newHaving, \";\");\n\t\t\t\tif (havingss != null) {\n\t\t\t\t\tint ind = -1;\n\t\t\t\t\tfor (int i = 0; i < havingss.length; i++) {\n\n\t\t\t\t\t\tString havingsStr = havingss[i];\n\t\t\t\t\t\tint start = havingsStr == null ? -1 : havingsStr.indexOf(\"(\");\n\t\t\t\t\t\tint end = havingsStr == null ? -1 : havingsStr.lastIndexOf(\")\");\n\t\t\t\t\t\tif (IS_HAVING_ALLOW_NOT_FUNCTION == false && (start < 0 || start >= end)) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":value } 里的 value 中的第 \" + i +\n\t\t\t\t\t\t\t\t\t\" 个字符 '\" + havingsStr + \"' 不合法！里面没有包含 SQL 函数！必须为 fun(col1,col2..)?val 格式！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tString[] havings = start >= 0 && end > start ? new String[]{havingsStr} : StringUtil.split(havingsStr);\n\t\t\t\t\t\tif (havings != null) {\n\t\t\t\t\t\t\tfor (int j = 0; j < havings.length; j++) {\n\t\t\t\t\t\t\t\tind ++;\n\t\t\t\t\t\t\t\tString h = havings[j];\n\t\t\t\t\t\t\t\tif (StringUtil.isEmpty(h, true)) {\n\t\t\t\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":value } 里的\"\n\t\t\t\t\t\t\t\t\t\t\t+ \" value 中的第 \" + ind + \" 个字符 '\" + h + \"' 不合法！不允许为空！\");\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\thavingMap.put(\"having\" + ind, h);\n\n\t\t\t\t\t\t\t\tif (isHavingAnd == false && IS_HAVING_DEFAULT_AND == false) {\n\t\t\t\t\t\t\t\t\thavingCombine += (ind <= 0 ? \"\" : \" | \") + \"having\" + ind;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (newHaving instanceof Map<?, ?>) {\n\t\t\t\tif (isHavingAnd) {\n\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":value } 里的 value 类型不合法！\"\n\t\t\t\t\t\t\t+ \"@having&:value 中 value 只能是 String，@having:value 中 value 只能是 String 或 JSONMap ！\");\n\t\t\t\t}\n\n\t\t\t\tM havingObj = JSON.createJSONObject((Map<? extends String, ?>) newHaving);\n\t\t\t\tSet<Entry<String, Object>> havingSet = havingObj.entrySet();\n\t\t\t\tfor (Entry<String, Object> entry : havingSet) {\n\t\t\t\t\tString k = entry == null ? null : entry.getKey();\n\t\t\t\t\tObject v = k == null ? null : entry.getValue();\n\t\t\t\t\tif (v == null) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (v instanceof String == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":{ \" + k + \":value } } 里的\"\n\t\t\t\t\t\t\t\t+ \" value 不合法！类型只能是 String，且不允许为空！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (ignoreEmptyOrBlankStr && StringUtil.isEmpty(v, ignoreBlankStr)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (KEY_COMBINE.equals(k)) {\n\t\t\t\t\t\thavingCombine = (String) v;\n\t\t\t\t\t}\n\t\t\t\t\telse if (StringUtil.isName(k) == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":{ \" + k + \":value } } 里的\"\n\t\t\t\t\t\t\t\t+ \" key 对应字符 \" + k + \" 不合法！必须为 英文字母 开头，且只包含 英文字母、下划线、数字 的合法变量名！\");\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\thavingMap.put(k, (String) v);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (newHaving != null) {\n\t\t\t\tthrow new IllegalArgumentException(table + \":{ \" + havingKey +  \":value } 里的 value 类型不合法！\"\n\t\t\t\t\t\t+ \"@having:value 中 value 只能是 String 或 JSONMap，@having&:value 中 value 只能是 String ！\");\n\t\t\t}\n\t\t\t// @having, @haivng& >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\t\t\tif (keyMap instanceof Map) {\n\t\t\t\tconfig.setKeyMap((Map<String, String>) keyMap);\n\t\t\t}\n\t\t\telse if (keyMap instanceof String) {\n\t\t\t\tString[] ks = StringUtil.split((String) keyMap, \";\");\n\t\t\t\tif (ks.length > 0) {\n\t\t\t\t\tMap<String, String> nkm = new LinkedHashMap<>();\n\t\t\t\t\tfor (int i = 0; i < ks.length; i++) {\n\t\t\t\t\t\tEntry<String, String> ety = Pair.parseEntry(ks[i]);\n\t\t\t\t\t\tif (ety == null) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnkm.put(ety.getKey(), ety.getValue());\n\t\t\t\t\t}\n\t\t\t\t\tconfig.setKeyMap(nkm);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse if (keyMap != null) {\n\t\t\t\tthrow new UnsupportedDataTypeException(\"@key:value 中 value 错误，只能是 String, JSONMap 中的一种！\");\n\t\t\t}\n\n\n\t\t\tconfig.setExplain(explain != null && explain);\n\t\t\tconfig.setCache(getCache(cache));\n\t\t\tconfig.setDistinct(distinct);\n\t\t\tconfig.setColumn(column == null ? null : cs); //解决总是 config.column != null，总是不能得到 *\n\t\t\tconfig.setFrom(from);\n\n\t\t\tconfig.setRole(role);\n\t\t\tconfig.setId(id);\n\t\t\tconfig.setIdIn(idIn);\n\t\t\tconfig.setUserId(userId);\n\t\t\tconfig.setUserIdIn(userIdIn);\n\n\t\t\tconfig.setNull(nullKeys == null || nullKeys.length <= 0 ? null : new ArrayList<>(Arrays.asList(nullKeys)));\n\t\t\tconfig.setCast(castMap);\n\t\t\tconfig.setWhere(tableWhere);\n\t\t\tconfig.setGroup(group);\n\t\t\tconfig.setHaving(havingMap);\n\t\t\tconfig.setHavingCombine(havingCombine);\n\t\t\tconfig.setSample(sample);\n\t\t\tconfig.setLatest(latest);\n\t\t\tconfig.setPartition(partition);\n\t\t\tconfig.setFill(fill);\n\t\t\tconfig.setOrder(order);\n\n\t\t\tString[] jsons = StringUtil.split(json);\n\t\t\tconfig.setJson(jsons == null || jsons.length <= 0 ? null : new ArrayList<>(Arrays.asList(jsons)));\n\n\t\t}\n\t\tfinally {  // 后面还可能用到，要还原\n\t\t\t// id, id{}, userId, userIdIn 条件\n\t\t\tif (id != null) {\n\t\t\t\trequest.put(idKey, id);\n\t\t\t}\n\t\t\tif (idIn != null) {\n\t\t\t\trequest.put(idInKey, idIn);\n\t\t\t}\n\t\t\tif (userId != null) {\n\t\t\t\trequest.put(userIdKey, userId);\n\t\t\t}\n\t\t\tif (userIdIn != null) {\n\t\t\t\trequest.put(userIdInKey, userIdIn);\n\t\t\t}\n\n\t\t\t// 关键词\n\t\t\tif (role != null) {\n\t\t\t\trequest.put(KEY_ROLE, role);\n\t\t\t}\n\t\t\tif (explain != null) {\n\t\t\t\trequest.put(KEY_EXPLAIN, explain);\n\t\t\t}\n\t\t\tif (cache != null) {\n\t\t\t\trequest.put(KEY_CACHE, cache);\n\t\t\t}\n\t\t\tif (database != null) {\n\t\t\t\trequest.put(KEY_DATABASE, database);\n\t\t\t}\n\t\t\tif (datasource != null) {\n\t\t\t\trequest.put(KEY_DATASOURCE, datasource);\n\t\t\t}\n\t\t\tif (schema != null) {\n\t\t\t\trequest.put(KEY_SCHEMA, schema);\n\t\t\t}\n\t\t\tif (from != null) {\n\t\t\t\trequest.put(KEY_FROM, from);\n\t\t\t}\n\t\t\tif (column != null) {\n\t\t\t\trequest.put(KEY_COLUMN, column);\n\t\t\t}\n\t\t\tif (nulls != null) {\n\t\t\t\trequest.put(KEY_NULL, nulls);\n\t\t\t}\n\t\t\tif (cast != null) {\n\t\t\t\trequest.put(KEY_CAST, cast);\n\t\t\t}\n\t\t\tif (combine != null) {\n\t\t\t\trequest.put(KEY_COMBINE, combine);\n\t\t\t}\n\t\t\tif (group != null) {\n\t\t\t\trequest.put(KEY_GROUP, group);\n\t\t\t}\n\t\t\tif (having != null) {\n\t\t\t\trequest.put(KEY_HAVING, having);\n\t\t\t}\n\t\t\tif (havingAnd != null) {\n\t\t\t\trequest.put(KEY_HAVING_AND, havingAnd);\n\t\t\t}\n\t\t\tif (sample != null) {\n\t\t\t\trequest.put(KEY_SAMPLE, sample);\n\t\t\t}\n\t\t\tif (latest != null) {\n\t\t\t\trequest.put(KEY_LATEST, latest);\n\t\t\t}\n\t\t\tif (partition != null) {\n\t\t\t\trequest.put(KEY_PARTITION, partition);\n\t\t\t}\n\t\t\tif (fill != null) {\n\t\t\t\trequest.put(KEY_FILL, fill);\n\t\t\t}\n\t\t\tif (order != null) {\n\t\t\t\trequest.put(KEY_ORDER, order);\n\t\t\t}\n\t\t\tif (keyMap != null) {\n\t\t\t\trequest.put(KEY_KEY, keyMap);\n\t\t\t}\n\t\t\tif (raw != null) {\n\t\t\t\trequest.put(KEY_RAW, raw);\n\t\t\t}\n\t\t\tif (json != null) {\n\t\t\t\trequest.put(KEY_JSON, json);\n\t\t\t}\n\t\t\tif (mthd != null) {\n\t\t\t\trequest.put(KEY_METHOD, mthd);\n\t\t\t}\n\t\t}\n\n\t\treturn config;\n\t}\n\n\n\n\t/**\n\t * @param method\n\t * @param config\n\t * @param joinList\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> SQLConfig<T, M, L> parseJoin(\n\t\t\tRequestMethod method, SQLConfig<T, M, L> config, List<Join<T, M, L>> joinList, Callback<T, M, L> callback) throws Exception {\n\t\tboolean isQuery = RequestMethod.isQueryMethod(method);\n\t\tconfig.setKeyPrefix(isQuery && config.isMain() == false);\n\n\t\t//TODO 解析出 SQLConfig<T, M, L> 再合并 column, order, group 等\n\t\tif (joinList == null || joinList.isEmpty() || RequestMethod.isQueryMethod(method) == false) {\n\t\t\treturn config;\n\t\t}\n\n\n\t\tString table;\n\t\tString alias;\n\t\tfor (Join<T, M, L> join : joinList) {\n\t\t\ttable = join.getTable();\n\t\t\talias = join.getAlias();\n\t\t\t//JOIN子查询不能设置LIMIT，因为ON关系是在子查询后处理的，会导致结果会错误\n\t\t\tSQLConfig<T, M, L> joinConfig = newSQLConfig(method, table, alias, join.getRequest(), null, false, callback);\n\t\t\tSQLConfig<T, M, L> cacheConfig = join.canCacheViceTable() == false ? null : newSQLConfig(method, table, alias\n\t\t\t\t\t, join.getRequest(), null, false, callback).setCount(join.getCount());\n\n\t\t\tif (join.isAppJoin() == false) { //除了 @ APP JOIN，其它都是 SQL JOIN，则副表要这样配置\n\t\t\t\tif (joinConfig.getDatabase() == null) {\n\t\t\t\t\tjoinConfig.setDatabase(config.getDatabase()); //解决主表 JOIN 副表，引号不一致\n\t\t\t\t}\n\t\t\t\telse if (joinConfig.getDatabase().equals(config.getDatabase()) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"主表 \" + config.getTable() + \" 的 @database:\" + config.getDatabase()\n\t\t\t\t\t\t\t+ \" 和它 SQL JOIN 的副表 \" + table + \" 的 @database:\" + joinConfig.getDatabase() + \" 不一致！\");\n\t\t\t\t}\n\t\t\t\tif (joinConfig.getSchema() == null) {\n\t\t\t\t\tjoinConfig.setSchema(config.getSchema()); //主表 JOIN 副表，默认 schema 一致\n\t\t\t\t}\n\n\t\t\t\tif (cacheConfig != null) {\n\t\t\t\t\tcacheConfig.setDatabase(joinConfig.getDatabase()).setSchema(joinConfig.getSchema()); //解决主表 JOIN 副表，引号不一致\n\t\t\t\t}\n\n\t\t\t\tif (isQuery) {\n\t\t\t\t\tconfig.setKeyPrefix(true);\n\t\t\t\t}\n\n\t\t\t\tjoinConfig.setMain(false).setKeyPrefix(true);\n\n\t\t\t\tif (join.getOn() != null) {\n\t\t\t\t\tSQLConfig<T, M, L> onConfig = newSQLConfig(method, table, alias, join.getOn(), null, false, callback);\n\t\t\t\t\tonConfig.setMain(false)\n\t\t\t\t\t\t\t.setKeyPrefix(true)\n\t\t\t\t\t\t\t.setDatabase(joinConfig.getDatabase())\n\t\t\t\t\t\t\t.setSchema(joinConfig.getSchema()); //解决主表 JOIN 副表，引号不一致\n\n\t\t\t\t\tjoin.setOnConfig(onConfig);\n\t\t\t\t}\n\n\t\t\t\tif (join.getOuter() != null) {\n\t\t\t\t\tSQLConfig<T, M, L> outerConfig = newSQLConfig(method, table, alias, join.getOuter(), null, false, callback);\n\t\t\t\t\touterConfig.setMain(false)\n\t\t\t\t\t\t\t.setKeyPrefix(true)\n\t\t\t\t\t\t\t.setDatabase(joinConfig.getDatabase())\n\t\t\t\t\t\t\t.setSchema(joinConfig.getSchema()); //解决主表 JOIN 副表，引号不一致\n\t\t\t\t\tjoin.setOuterConfig(outerConfig);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t//解决 query: 1/2 查数量时报错\n\t\t\t/* SELECT  count(*)  AS count  FROM sys.Moment AS Moment\n\t\t\t   LEFT JOIN ( SELECT count(*)  AS count FROM sys.Comment ) AS Comment ON Comment.momentId = Moment.id LIMIT 1 OFFSET 0 */\n\t\t\tif (RequestMethod.isHeadMethod(method, true)) {\n\t\t\t\tList<On> onList = join.getOnList();\n\t\t\t\tList<String> column = onList == null ? null : new ArrayList<>(onList.size());\n\t\t\t\t//解决 pg  如果只查询关联键，会报找不到column的错误\n\t\t\t\t///* SELECT  count(*)  AS count  FROM sys.Moment AS Moment\n\t\t\t\t//\t\t\t   LEFT JOIN ( SELECT *  FROM sys.Comment ) AS Comment ON Comment.momentId = Moment.id LIMIT 1 OFFSET 0 */\n\t\t\t\tif (column != null && joinConfig.isMSQL()) { // 暂时这样兼容 PostgreSQL 等不支持 SELECT 中不包含对应 key 的隐式 ON 关联字段的数据库\n\t\t\t\t\tfor (On on : onList) {\n\t\t\t\t\t\tcolumn.add(on.getKey()); // TODO PostgreSQL 等需要找到具体的 targetTable 对应 targetKey 来加到 SELECT，比直接 SELECT * 性能更好\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tjoinConfig.setMethod(GET);  // 子查询不能为 SELECT count(*) ，而应该是 SELECT momentId\n\t\t\t\tjoinConfig.setColumn(column);  // 优化性能，不取非必要的字段\n\n\t\t\t\tif (cacheConfig != null) {\n\t\t\t\t\tcacheConfig.setMethod(GET);  // 子查询不能为 SELECT count(*) ，而应该是 SELECT momentId\n\t\t\t\t\tcacheConfig.setColumn(column);  // 优化性能，不取非必要的字段\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tjoin.setJoinConfig(joinConfig);\n\t\t\tjoin.setCacheConfig(cacheConfig);\n\t\t}\n\n\t\tconfig.setJoinList(joinList);\n\n\t\treturn config;\n\t}\n\n\n\n\t/**获取客户端实际需要的key\n\t * verifyName = true\n\t * @param method\n\t * @param originKey\n\t * @param isTableKey\n\t * @param saveLogic 保留逻辑运算符 & | !\n\t * @return\n\t */\n\tpublic static String gainRealKey(RequestMethod method, String originKey\n\t\t\t, boolean isTableKey, boolean saveLogic) throws Exception {\n\t\treturn gainRealKey(method, originKey, isTableKey, saveLogic, true);\n\t}\n\t/**获取客户端实际需要的key\n\t * @param method\n\t * @param originKey\n\t * @param isTableKey\n\t * @param saveLogic 保留逻辑运算符 & | !\n\t * @param verifyName 验证key名是否符合代码变量/常量名\n\t * @return\n\t */\n\tpublic static String gainRealKey(RequestMethod method, String originKey\n\t\t\t, boolean isTableKey, boolean saveLogic, boolean verifyName) throws Exception {\n\t\tLog.i(TAG, \"getRealKey  saveLogic = \" + saveLogic + \"; originKey = \" + originKey);\n\t\tif (originKey == null || JSONMap.isArrayKey(originKey)) {\n\t\t\tLog.w(TAG, \"getRealKey  originKey == null || apijson.JSONMap.isArrayKey(originKey) >>  return originKey;\");\n\t\t\treturn originKey;\n\t\t}\n\n\t\tString key = originKey;\n\t\tif (key.endsWith(\"$\")) {//搜索 LIKE，查询时处理\n\t\t\tString k = key.substring(0, key.length() - 1);\n\t\t\t// key%$:\"a\" -> key LIKE '%a%'; key?%$:\"a\" -> key LIKE 'a%'; key_?$:\"a\" -> key LIKE '_a'; key_%$:\"a\" -> key LIKE '_a%'\n\t\t\tchar c = k.isEmpty() ? 0 : k.charAt(k.length() - 1);\n\n\t\t\tif (c == '%' || c == '_' || c == '?') {\n\t\t\t\tk = k.substring(0, k.length() - 1);\n\n\t\t\t\tchar c2 = k.isEmpty() ? 0 : k.charAt(k.length() - 1);\n\t\t\t\tif (c2 == '%' || c2 == '_' || c2 == '?') {\n\t\t\t\t\tif (c2 == c) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(originKey + \":value 中字符 \"\n\t\t\t\t\t\t\t\t+ k + \" 不合法！key$:value 中不允许 key 中有连续相同的占位符！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tk = k.substring(0, k.length() - 1);\n\t\t\t\t}\n\t\t\t\telse if (c == '?') {\n\t\t\t\t\tthrow new IllegalArgumentException(originKey + \":value 中字符 \" + originKey\n\t\t\t\t\t\t\t+ \" 不合法！key$:value 中不允许只有单独的 '?'，必须和 '%', '_' 之一配合使用 ！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tkey = k;\n\t\t}\n\t\telse if (key.endsWith(\"~\")) {//匹配正则表达式 REGEXP，查询时处理\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t\tif (key.endsWith(\"*\")) {//忽略大小写\n\t\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t\t}\n\t\t}\n\t\telse if (key.endsWith(\"%\")) {//数字、文本、日期范围 BETWEEN AND\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (key.endsWith(\"{}\")) {//被包含 IN，或者说key对应值处于value的范围内。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\"}{\")) {//被包含 EXISTS，或者说key对应值处于value的范围内。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\"<>\")) {//包含 json_contains，或者说value处于key对应值的范围内。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\"()\")) {//方法，查询完后处理，先用一个Map<key,function>保存\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\"@\")) {//引用，引用对象查询完后处理。fillTarget中暂时不用处理，因为非GET请求都是由给定的id确定，不需要引用\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (key.endsWith(\">=\")) {//比较。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\"<=\")) {//比较。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 2);\n\t\t}\n\t\telse if (key.endsWith(\">\")) {//比较。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (key.endsWith(\"<\")) {//比较。查询时处理\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\telse if (key.endsWith(\"+\")) {//延长，PUT查询时处理\n\t\t\tif (method == PUT) {//不为PUT就抛异常\n\t\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t\t}\n\t\t}\n\t\telse if (key.endsWith(\"-\")) {//缩减，PUT查询时处理\n\t\t\tif (method == PUT) {//不为PUT就抛异常\n\t\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t\t}\n\t\t}\n\n\t\t// TODO if (key.endsWith(\"-\")) { // 表示 key 和 value 顺序反过来: value LIKE key ?\n\n\t\t// 不用Logic优化代码，否则 key 可能变为 key| 导致 key=value 变成 key|=value 而出错\n\t\tString last = key.isEmpty() ? \"\" : key.substring(key.length() - 1);\n\t\tif (\"&\".equals(last) || \"|\".equals(last) || \"!\".equals(last)) {\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t} else {\n\t\t\tlast = null; // 避免key + StringUtil.getString(last) 错误延长\n\t\t}\n\n\t\tString len = \"\";\n\t\tif (key.endsWith(\"[\") || key.endsWith(\"{\")) {\n\t\t\tlen = key.substring(key.length() - 1);\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\n\t\t// \"User:toUser\":User转换\"toUser\":User, User为查询同名Table得到的JSONObject。交给客户端处理更好\n\t\tif (isTableKey) { // 不允许在column key中使用Type:key形式\n\t\t\tkey = Pair.parseEntry(key, true).getKey(); // table以左边为准\n\t\t} else {\n\t\t\tkey = Pair.parseEntry(key).getValue();// column 以右边为准\n\t\t}\n\n\t\tif (verifyName && StringUtil.isName(key.startsWith(\"@\") ? key.substring(1) : key) == false) {\n\t\t\tthrow new IllegalArgumentException(method + \"请求，字符 \" + originKey + \" 不合法！\"\n\t\t\t\t\t+ \" key:value 中的 key 只能关键词 '@key' 或 'key[长度符][逻辑符][条件符]' 或 PUT 请求下的 'key+' / 'key-' ！\"\n\t\t\t\t\t+ \"长度符 只能为 [ - length 和 { - json_length，逻辑符 只能是 & - 与、| - 或、! - 非 ！\");\n\t\t}\n\n\t\tkey += len;\n\n\t\tif (saveLogic && last != null) {\n\t\t\tkey = key + last;\n\t\t}\n\t\tLog.i(TAG, \"getRealKey  return key = \" + key);\n\t\treturn key;\n\t}\n\n\n\tpublic static interface IdCallback<T> {\n\t\t/**为 post 请求新建 id， 只能是 Long 或 String\n\t\t * @param method\n\t\t * @param database\n\t\t * @param schema\n\t\t * @param table\n\t\t * @return\n\t\t */\n\t\tT newId(RequestMethod method, String database, String schema, String datasource, String table);\n\n\n\t\t/**获取主键名\n\t\t * @param database\n\t\t * @param schema\n\t\t * @param table\n\t\t * @return\n\t\t */\n\t\tString getIdKey(String database, String schema, String datasource, String table);\n\n\t\t/**获取 User 的主键名\n\t\t * @param database\n\t\t * @param schema\n\t\t * @param table\n\t\t * @return\n\t\t */\n\t\tString getUserIdKey(String database, String schema, String datasource, String table);\n\t}\n\n\tpublic static interface Callback<T, M extends Map<String, Object>, L extends List<Object>> extends IdCallback<T> {\n\t\t/**获取 SQLConfig<T, M, L> 的实例\n\t\t * @param method\n\t\t * @param database\n\t\t * @param schema\n\t\t * @param table\n\t\t * @return\n\t\t */\n\t\tSQLConfig<T, M, L> getSQLConfig(RequestMethod method, String database, String schema, String datasource, String table);\n\n\t\t/**combine 里的 key 在 request 中 value 为 null 或不存在，即 request 中缺少用来作为 combine 条件的 key: value\n\t\t * @param combine\n\t\t * @param key\n\t\t * @param request\n\t\t */\n\t\tvoid onMissingKey4Combine(String name, M request, String combine, String item, String key) throws Exception;\n\t}\n\n\tpublic static Long LAST_ID;\n\tstatic {\n\t\tLAST_ID = System.currentTimeMillis();\n\t}\n\n\tpublic static abstract class SimpleCallback<T, M extends Map<String, Object>, L extends List<Object>> implements Callback<T, M, L> {\n\n\t\t@SuppressWarnings(\"unchecked\")\n\t\t@Override\n\t\tpublic T newId(RequestMethod method, String database, String schema, String datasource, String table) {\n\t\t\tLong id = System.currentTimeMillis();\n\t\t\tif (id <= LAST_ID) {\n\t\t\t\tid = LAST_ID + 1; // 解决高并发下 id 冲突导致新增记录失败\n\t\t\t}\n\t\t\tLAST_ID = id;\n\n\t\t\treturn (T) id;\n\t\t}\n\n\t\t@Override\n\t\tpublic String getIdKey(String database, String schema, String datasource, String table) {\n\t\t\treturn KEY_ID;\n\t\t}\n\n\t\t@Override\n\t\tpublic String getUserIdKey(String database, String schema, String datasource, String table) {\n\t\t\treturn KEY_USER_ID;\n\t\t}\n\n\t\t@Override\n\t\tpublic void onMissingKey4Combine(String name, M request, String combine, String item, String key) throws Exception {\n\t\t\tif (ALLOW_MISSING_KEY_4_COMBINE) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthrow new IllegalArgumentException(name + \":{} 里的 @combine:value 中的value里 \"\n\t\t\t\t\t+ item + \" 对应的条件 \" + key + \":value 中 value 必须存在且不能为 null！\");\n\t\t}\n\n\t}\n\n\tprivate static boolean isKeyInCombineExpr(String combineExpr, String key) {\n\t\twhile (combineExpr.isEmpty() == false) {\n\t\t\tint index = combineExpr.indexOf(key);\n\t\t\tif (index < 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tchar left = index <= 0 ? ' ' : combineExpr.charAt(index - 1);\n\t\t\tchar right = index >= combineExpr.length() - key.length() ? ' ' : combineExpr.charAt(index + key.length());\n\t\t\tif ((left == ' ' || left == '(' || left == '&' || left == '|' || left == '!') && (right == ' ' || right == ')' || right == ':')) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tint newIndex = index + key.length() + 1;\n\t\t\tif (combineExpr.length() <= newIndex) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcombineExpr = combineExpr.substring(newIndex);\n\t\t}\n\n\t\treturn false;\n\t}\n\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\r\n\r\nThis source code is licensed under the Apache License Version 2.0.*/\r\n\r\n\r\npackage apijson.orm;\r\n\r\nimport apijson.*;\r\nimport apijson.orm.Join.On;\r\nimport apijson.orm.exception.NotExistException;\r\n\r\nimport java.io.BufferedReader;\r\nimport java.math.BigDecimal;\r\nimport java.math.BigInteger;\r\nimport java.sql.*;\r\nimport java.time.DayOfWeek;\r\nimport java.time.LocalDateTime;\r\nimport java.time.Month;\r\nimport java.time.Year;\r\nimport java.util.Date;\r\nimport java.util.*;\r\nimport java.util.Map.Entry;\r\nimport java.util.regex.Pattern;\r\n\r\n/**executor for query(read) or update(write) MySQL database\r\n * @author Lemon\r\n */\r\npublic abstract class AbstractSQLExecutor<T, M extends Map<String, Object>, L extends List<Object>>\r\n\t\timplements SQLExecutor<T, M, L> {\r\n\tprivate static final String TAG = \"AbstractSQLExecutor\";\r\n\t//是否返回 值为null的字段\r\n\tpublic static boolean ENABLE_OUTPUT_NULL_COLUMN = false;\r\n\tpublic static String KEY_RAW_LIST = \"@RAW@LIST\";  // 避免和字段命名冲突，不用 $RAW@LIST$ 是因为 $ 会在 fastjson 内部转义，浪费性能\r\n\tpublic static String KEY_VICE_ITEM = \"@VICE@ITEM\";  // 避免和字段命名冲突，不用 $VICE@LIST$ 是因为 $ 会在 fastjson 内部转义，浪费性能\r\n\r\n\tprivate Parser<T, M, L> parser;\r\n\t@Override\r\n\tpublic Parser<T, M, L> getParser() {\r\n\t\treturn parser;\r\n\t}\r\n\t@Override\r\n\tpublic AbstractSQLExecutor<T, M, L> setParser(Parser<T, M, L> parser) {\r\n\t\tthis.parser = parser;\r\n\t\treturn this;\r\n\t}\r\n\r\n\tprivate int generatedSQLCount = 0;\r\n\tprivate int cachedSQLCount = 0;\r\n\tprivate int executedSQLCount = 0;\r\n\r\n\t@Override\r\n\tpublic int getGeneratedSQLCount() {\r\n\t\treturn generatedSQLCount;\r\n\t}\r\n\t@Override\r\n\tpublic int getCachedSQLCount() {\r\n\t\treturn cachedSQLCount;\r\n\t}\r\n\t@Override\r\n\tpublic int getExecutedSQLCount() {\r\n\t\treturn executedSQLCount;\r\n\t}\r\n\r\n\tprivate long executedSQLDuration = 0;\r\n\tprivate long sqlResultDuration = 0;\r\n\t@Override\r\n\tpublic long getExecutedSQLDuration() {\r\n\t\treturn executedSQLDuration;\r\n\t}\r\n\r\n\t@Override\r\n\tpublic long getSqlResultDuration() {\r\n\t\treturn sqlResultDuration;\r\n\t}\r\n\r\n\r\n\t/**\r\n\t * 缓存 Map\r\n\t */\r\n\tprotected Map<String, List<M>> cacheMap = new HashMap<>();\r\n\r\n\t/**保存缓存\r\n\t * @param sql  key\r\n\t * @param list  value\r\n\t * @param config  一般主表 SQLConfig<T, M, L> 不为 null，JOIN 副表的为 null\r\n\t */\r\n\t@Override\r\n\tpublic void putCache(String sql, List<M> list, SQLConfig<T, M, L> config) {\r\n\t\tif (sql == null || list == null) { // 空 list 有效，说明查询过 sql 了  || list.isEmpty()) {\r\n\t\t\tLog.i(TAG, \"saveList  sql == null || list == null >> return;\");\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tcacheMap.put(sql, list);\r\n\t}\r\n\r\n\t/**获取缓存\r\n\t * @param sql  key\r\n\t * @param config  一般主表 SQLConfig<T, M, L> 不为 null，JOIN 副表的为 null\r\n\t */\r\n\t@Override\r\n\tpublic List<M> getCache(String sql, SQLConfig<T, M, L> config) {\r\n\t\treturn cacheMap.get(sql);\r\n\t}\r\n\r\n\t/**获取缓存\r\n\t * @param sql  key\r\n\t * @param position\r\n\t * @param config  一般主表 SQLConfig<T, M, L> 不为 null，JOIN 副表的为 null\r\n\t * @return\r\n\t */\r\n\t@Override\r\n\tpublic M getCacheItem(String sql, int position, SQLConfig<T, M, L> config) {\r\n\t\tList<M> list = getCache(sql, config);\r\n\t\treturn getCacheItem(list, position, config);\r\n\t}\r\n\r\n\tpublic M getCacheItem(List<M> list, int position, SQLConfig<T, M, L> config) {\r\n\t\t// 只要 list 不为 null，则如果 list.get(position) == null，则返回 {} ，避免再次 SQL 查询\r\n\t\tif (list == null) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tM result = position >= list.size() ? null : list.get(position);\r\n\t\treturn result != null ? result : JSON.createJSONObject();\r\n\t}\r\n\r\n\r\n\r\n\t/**移除缓存\r\n\t * @param sql  key\r\n\t * @param config\r\n\t */\r\n\t@Override\r\n\tpublic void removeCache(String sql, SQLConfig<T, M, L> config) {\r\n\t\tif (sql == null) {\r\n\t\t\tLog.i(TAG, \"removeList  sql == null >> return;\");\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tcacheMap.remove(sql);\r\n\t}\r\n\r\n\r\n\t@Override\r\n\tpublic ResultSet executeQuery(@NotNull Statement statement, String sql) throws Exception {\r\n\t\tResultSet rs = statement.executeQuery(sql);\r\n\t\treturn rs;\r\n\t}\r\n\t@Override\r\n\tpublic int executeUpdate(@NotNull Statement statement, String sql) throws Exception {\r\n\t\tint c = statement.executeUpdate(sql);\r\n\t\treturn c;\r\n\t}\r\n\t@Override\r\n\tpublic ResultSet execute(@NotNull Statement statement, String sql) throws Exception {\r\n\t\tstatement.execute(sql);\r\n\t\tResultSet rs = statement.getResultSet();\r\n\t\treturn rs;\r\n\t}\r\n\r\n\t/**执行SQL\r\n\t * @param config\r\n\t * @return\r\n\t * @throws Exception\r\n\t */\r\n\t@Override\r\n\tpublic M execute(@NotNull SQLConfig<T, M, L> config, boolean unknownType) throws Exception {\r\n\t\tlong executedSQLStartTime = System.currentTimeMillis();\r\n\t\tfinal String sql = config.gainSQL(false);\r\n\r\n\t\tif (StringUtil.isEmpty(sql, true)) {\r\n\t\t\tLog.e(TAG, \"execute  StringUtil.isEmpty(sql, true) >> return null;\");\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tParser<T, M, L> parser2 = config.gainParser();\r\n\t\tparser = parser2 != null ? parser2 : getParser();;\r\n\r\n\t\tboolean isExplain = config.isExplain();\r\n\t\tboolean isHead = RequestMethod.isHeadMethod(config.getMethod(), true);\r\n\r\n\t\tfinal int position = config.getPosition();\r\n\t\tM result;\r\n\r\n\t\tif (isExplain == false) {\r\n\t\t\tgeneratedSQLCount ++;\r\n\t\t}\r\n\r\n\t\tlong startTime = System.currentTimeMillis();\r\n\t\tLog.d(TAG, \"\\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\"\r\n\t\t\t\t+ \"\\n已生成 \" + generatedSQLCount + \" 条 SQL\"\r\n\t\t\t\t+ \"\\nexecute  startTime = \" + startTime\r\n\t\t\t\t+ \"\\ndatabase = \" + StringUtil.get(config.getDatabase())\r\n\t\t\t\t+ \"; schema = \" + StringUtil.get(config.getSchema())\r\n\t\t\t\t+ \"; sql = \\n\" + sql\r\n\t\t\t\t+ \"\\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\");\r\n\r\n\t\tResultSet rs = null;\r\n\t\tList<M> resultList = null;\r\n\t\tMap<String, M> childMap = null;\r\n\t\tMap<String, String> keyMap = null;\r\n\r\n\t\ttry {\r\n\t\t\tif (unknownType) {\r\n\t\t\t\tif (isExplain == false) { //只有 SELECT 才能 EXPLAIN\r\n\t\t\t\t\texecutedSQLCount ++;\r\n\t\t\t\t\texecutedSQLStartTime = System.currentTimeMillis();\r\n\t\t\t\t}\r\n\t\t\t\tStatement statement = getStatement(config);\r\n\t\t\t\trs = execute(statement, sql);\r\n\t\t\t\tint updateCount = statement.getUpdateCount();\r\n\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\texecutedSQLDuration += System.currentTimeMillis() - executedSQLStartTime;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tresult = JSON.createJSONObject();\r\n\t\t\t\tresult.put(JSONResponse.KEY_COUNT, updateCount);\r\n\t\t\t\tresult.put(\"update\", updateCount >= 0);\r\n\t\t\t\t//导致后面 rs.getMetaData() 报错 Operation not allowed after ResultSet closed\t\tresult.put(\"moreResults\", statement.getMoreResults());\r\n\t\t\t}\r\n\t\t\telse {\r\n\t\t\t\tRequestMethod method = config.getMethod();\r\n\t\t\t\tswitch (method) {\r\n\t\t\t\tcase POST:\r\n\t\t\t\tcase PUT:\r\n\t\t\t\tcase DELETE:\r\n\t\t\t\t\tif (isExplain == false) { //只有 SELECT 才能 EXPLAIN\r\n\t\t\t\t\t\texecutedSQLCount ++;\r\n\t\t\t\t\t\texecutedSQLStartTime = System.currentTimeMillis();\r\n\t\t\t\t\t}\r\n\t\t\t\t\tint updateCount = executeUpdate(config);\r\n\t\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\t\texecutedSQLDuration += System.currentTimeMillis() - executedSQLStartTime;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (updateCount <= 0) {\r\n\t\t\t\t\t\tthrow new IllegalAccessException(\"没权限访问或对象不存在！\");  // NotExistException 会被 catch 转为成功状态\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t// updateCount>0时收集结果。例如更新操作成功时，返回count(affected rows)、id字段\r\n\t\t\t\t\tresult = parser.newSuccessResult();  // TODO 对 APIAuto 及其它现有的前端/客户端影响比较大，暂时还是返回 code 和 msg，5.0 再移除  JSON.createJSONObject();\r\n\r\n\t\t\t\t\t//id,id{}至少一个会有，一定会返回，不用抛异常来阻止关联写操作时前面错误导致后面无条件执行！\r\n\t\t\t\t\tresult.put(JSONResponse.KEY_COUNT, updateCount);//返回修改的记录数\r\n\r\n\t\t\t\t\tString idKey = config.getIdKey();\r\n\t\t\t\t\tif (config.getId() != null) {\r\n\t\t\t\t\t\tresult.put(idKey, config.getId());\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (config.getIdIn() != null) {\r\n\t\t\t\t\t\tresult.put(idKey + \"[]\", config.getIdIn());\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (method == RequestMethod.PUT || method == RequestMethod.DELETE) {\r\n\t\t\t\t\t\tconfig.setMethod(RequestMethod.GET);\r\n\t\t\t\t\t\tremoveCache(config.gainSQL(false), config);\r\n\t\t\t\t\t\tconfig.setMethod(method);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\treturn result;\r\n\r\n\t\t\t\tcase GET:\r\n\t\t\t\tcase GETS:\r\n\t\t\t\tcase HEAD:\r\n\t\t\t\tcase HEADS:\r\n\t\t\t\t\tList<M> cache = getCache(sql, config);\r\n\t\t\t\t\tresult = getCacheItem(cache, position, config);\r\n\t\t\t\t\tLog.i(TAG, \">>> execute  result = getCache('\" + sql + \"', \" + position + \") = \" + result);\r\n\t\t\t\t\tif (result != null) {\r\n\t\t\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\t\t\tcachedSQLCount ++;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif (cache != null && cache.size() > 1) {\r\n\t\t\t\t\t\t\tresult.put(KEY_RAW_LIST, cache);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tLog.d(TAG, \"\\n\\n execute  result != null >> return result;\"  + \"\\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\\n\");\r\n\t\t\t\t\t\treturn result;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (isExplain == false) { //只有 SELECT 才能 EXPLAIN\r\n\t\t\t\t\t\texecutedSQLCount ++;\r\n\t\t\t\t\t\texecutedSQLStartTime = System.currentTimeMillis();\r\n\t\t\t\t\t}\r\n\t\t\t\t\trs = executeQuery(config);  //FIXME SQL Server 是一次返回两个结果集，包括查询结果和执行计划，需要 moreResults\r\n\t\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\t\texecutedSQLDuration += System.currentTimeMillis() - executedSQLStartTime;\r\n\t\t\t\t\t}\r\n\t\t\t\t\tbreak;\r\n\r\n\t\t\t\tdefault: //OPTIONS, TRACE等\r\n\t\t\t\t\tLog.e(TAG, \"execute  sql = \" + sql + \" ; method = \" + config.getMethod() + \" >> return null;\");\r\n\t\t\t\t\treturn null;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif (isExplain == false && isHead) {\r\n\t\t\t\tif (rs.next() == false) {\r\n\t\t\t\t\treturn parser.newErrorResult(new SQLException(\"数据库错误, rs.next() 失败！\"));\r\n\t\t\t\t}\r\n\r\n\t\t\t\tresult = parser.newSuccessResult();\r\n\t\t\t\t// 兼容nosql,比如 elasticSearch-sql\r\n\t\t\t\tif(config.isElasticsearch()) {\r\n\t\t\t\t\tresult.put(JSONResponse.KEY_COUNT, rs.getObject(1));\r\n\t\t\t\t}else {\r\n\t\t\t\t\tresult.put(JSONResponse.KEY_COUNT, rs.getLong(1));\r\n\t\t\t\t}\r\n\t\t\t\tresultList = new ArrayList<>(1);\r\n\t\t\t\tresultList.add(result);\r\n\t\t\t}\r\n\t\t\telse {\r\n\t\t\t\t//\t\tfinal boolean cache = config.getCount() != 1;\r\n\t\t\t\t//\t\tLog.d(TAG, \"select  cache = \" + cache + \"; resultList\" + (resultList == null ? \"=\" : \"!=\") + \"null\");\r\n\t\t\t\ttry {  // 设置初始容量为查到的数据量，解决频繁扩容导致的延迟，貌似只有 rs.last 取 rs.getRow() ? 然后又得 rs.beforeFirst 重置位置以便下方取值\r\n\t\t\t\t\trs.last();  //移到最后一行\r\n\t\t\t\t\tresultList = new ArrayList<>(rs.getRow());\r\n\t\t\t\t\trs.beforeFirst();\r\n\t\t\t\t}\r\n\t\t\t\tcatch (Throwable e) {\r\n\t\t\t\t\tLog.e(TAG, \"try { rs.last(); resultList = new ArrayList<>(rs.getRow()); rs.beforeFirst(); >> } catch (Throwable e) = \" + e.getMessage());\r\n\t\t\t\t\tint capacity;\r\n\t\t\t\t\tif (config.getId() != null) {  // id:Object 一定是 AND 条件，最终返回数据最多就这么多\r\n\t\t\t\t\t\tcapacity = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t\telse {\r\n\t\t\t\t\t\tObject idIn = config.getIdIn();\r\n\t\t\t\t\t\tif (idIn instanceof Collection<?>) {  // id{}:[] 一定是 AND 条件，最终返回数据最多就这么多\r\n\t\t\t\t\t\t\tcapacity = ((Collection<?>) idIn).size();\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\telse {  // 预估容量\r\n\t\t\t\t\t\t\tcapacity = config.getCount() <= 0 ? AbstractParser.MAX_QUERY_COUNT : config.getCount();\r\n\t\t\t\t\t\t\tif (capacity > 100) {\r\n\t\t\t\t\t\t\t\t// 有 WHERE 条件，条件越多过滤数据越多，暂时不考虑 @combine:\"a | (b & !c)\" 里面 | OR 和 ! NOT 条件，太复杂也不是很必要\r\n\t\t\t\t\t\t\t\tMap<String, List<String>> combine = config.getCombineMap();\r\n\r\n\t\t\t\t\t\t\t\tList<String> andList = combine == null ? null : combine.get(\"&\");\r\n\t\t\t\t\t\t\t\tint andCondCount = andList == null ? (config.getWhere() == null ? 0 : config.getWhere().size()) : andList.size();\r\n\r\n\t\t\t\t\t\t\t\tList<String> orList = combine == null ? null : combine.get(\"|\");\r\n\t\t\t\t\t\t\t\tint orCondCount = orList == null ? 0 : orList.size();\r\n\r\n\t\t\t\t\t\t\t\tList<String> notList = combine == null ? null : combine.get(\"!\");\r\n\t\t\t\t\t\t\t\tint notCondCount = notList == null ? 0 : notList.size();\r\n\r\n\t\t\t\t\t\t\t\t// 有 GROUP BY 分组，字段越少过滤数据越多\r\n\t\t\t\t\t\t\t\tString[] group = StringUtil.split(config.getGroup());\r\n\t\t\t\t\t\t\t\tint groupCount = group == null ? 0 : group.length;\r\n\t\t\t\t\t\t\t\tif (groupCount > 0 && Arrays.asList(group).contains(config.getIdKey())) {\r\n\t\t\t\t\t\t\t\t\tgroupCount = 0;\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t// 有 HAVING 聚合函数，字段越多过滤数据越多，暂时不考虑 @combine:\"a | (b & !c)\" 里面 | OR 和 ! NOT 条件，太复杂也不是很必要\r\n\t\t\t\t\t\t\t\tMap<String, Object> having = config.getHaving();\r\n\t\t\t\t\t\t\t\tint havingCount = having == null ? 0 : having.size();\r\n\r\n\t\t\t\t\t\t\t\tcapacity /= Math.pow(1.5, Math.log10(capacity)\r\n\t\t\t\t\t\t\t\t\t\t+ andCondCount\r\n\t\t\t\t\t\t\t\t\t\t+ ((orCondCount <= 0 ? 0 : 2.0d/orCondCount)  // 1: 2.3, 2: 1.5, 3: 1.3, 4: 1.23, 5: 1.18\r\n\t\t\t\t\t\t\t\t\t\t+ (notCondCount/5.0d)  // 1: 1.08, 2: 1.18, 3: 1.28, 4: 1.38, 1.50\r\n\t\t\t\t\t\t\t\t\t\t+ (groupCount <= 0 ? 0 : 10.0d/groupCount))  // 1: 57.7, 7.6, 3: 3.9, 4: 2.8, 5: 2.3\r\n\t\t\t\t\t\t\t\t\t\t+ havingCount\r\n\t\t\t\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t\t\tcapacity += 1;  // 避免正好比需要容量少一点点导致多一次扩容，大量数据 System.arrayCopy\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tresultList = new ArrayList<>(capacity);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tint index = -1;\r\n\r\n\t\t\t\tlong startTime2 = System.currentTimeMillis();\r\n\t\t\t\tResultSetMetaData rsmd = rs.getMetaData();\r\n\t\t\t\tfinal int length = rsmd.getColumnCount();\r\n\t\t\t\tsqlResultDuration += System.currentTimeMillis() - startTime2;\r\n\r\n\t\t\t\t//<SELECT * FROM Comment WHERE momentId = '470', { id: 1, content: \"csdgs\" }>\r\n\t\t\t\tchildMap = new HashMap<>(); //要存到cacheMap\r\n//\t\t\t\tMap<Integer, Join> columnIndexAndJoinMap = new HashMap<>(length);\r\n\t\t\t\tString lastTableName = null;  // 默认就是主表 config.getTable();\r\n\t\t\t\tString lastAliasName = null;  // 默认就是主表 config.getAlias();\r\n\t\t\t\tint lastViceTableStart = 0;\r\n\t\t\t\tint lastViceColumnStart = 0;\r\n\t\t\t\tJoin lastJoin = null;\r\n\t\t\t\t// TODO\t\t\t\tString[] columnIndexAndTableMap = new String[length];\r\n\t\t\t\t// WHERE id = ? AND ... 或 WHERE ... AND id = ? 强制排序 remove 再 put，还是重新 getSQL吧\r\n\r\n\r\n\t\t\t\tList<Join<T, M, L>> joinList = config.getJoinList();\r\n\t\t\t\tboolean hasJoin = config.hasJoin() && joinList != null && ! joinList.isEmpty();\r\n\r\n\t\t\t\t// 直接用数组存取更快  Map<Integer, Join> columnIndexAndJoinMap = isExplain || ! hasJoin ? null : new HashMap<>(length);\r\n\t\t\t\tJoin[] columnIndexAndJoinMap = isExplain || ! hasJoin ? null : new Join[length];\r\n\t\t\t\tMap<String, Integer> repeatMap = columnIndexAndJoinMap == null || ! config.isQuestDB() ? null : new HashMap<>();\r\n\t\t\t\tkeyMap = repeatMap == null ? null : new HashMap<>();\r\n\r\n//\t\t\t\tint viceColumnStart = length + 1; //第一个副表字段的index\r\n\r\n//\t\t\t\tFIXME 统计游标查找的时长？可能 ResultSet.next() 及 getTableName, getColumnName, getObject 比较耗时，因为不是一次加载到内存，而是边读边发\r\n\r\n\t\t\t\tlong lastCursorTime = System.currentTimeMillis();\r\n\t\t\t\twhile (rs.next()) {\r\n\t\t\t\t\tsqlResultDuration += System.currentTimeMillis() - lastCursorTime;\r\n\t\t\t\t\tlastCursorTime = System.currentTimeMillis();\r\n\r\n\t\t\t\t\tindex ++;\r\n\t\t\t\t\tLog.d(TAG, \"\\n\\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n execute while (rs.next()){  index = \" + index + \"\\n\\n\");\r\n\r\n\t\t\t\t\tM item = JSON.createJSONObject();\r\n\t\t\t\t\tM viceItem = null;\r\n\t\t\t\t\tM curItem = item;\r\n\t\t\t\t\tboolean isMain = true;\r\n\t\t\t\t\tboolean reseted = false;\r\n\r\n\t\t\t\t\tfor (int i = 1; i <= length; i++) {\r\n\r\n\t\t\t\t\t\t// if (hasJoin && viceColumnStart > length && config.getSQLTable().equalsIgnoreCase(rsmd.getTableName(i)) == false) {\r\n\t\t\t\t\t\t// \tviceColumnStart = i;\r\n\t\t\t\t\t\t// }\r\n\r\n\t\t\t\t\t\t// bugfix-修复非常规数据库字段，获取表名失败导致输出异常\r\n\t\t\t\t\t\tJoin curJoin = columnIndexAndJoinMap == null ? null : columnIndexAndJoinMap[i - 1];  // columnIndexAndJoinMap.get(i);\r\n\r\n\t\t\t\t\t\t// 为什么 isExplain == false 不用判断？因为所有字段都在一张 Query Plan 表\r\n\t\t\t\t\t\tif (index <= 0 && columnIndexAndJoinMap != null) { // && viceColumnStart > length) {\r\n\r\n\t\t\t\t\t\t\tSQLConfig<T, M, L> curConfig = curJoin == null || ! curJoin.isSQLJoin() ? null : curJoin.getCacheConfig();\r\n\t\t\t\t\t\t\tList<String> curColumn = curConfig == null ? null : curConfig.getColumn();\r\n\t\t\t\t\t\t\tString sqlTable = curConfig == null ? null : curConfig.gainSQLTable();\r\n\t\t\t\t\t\t\tString sqlAlias = curConfig == null ? null : curConfig.getAlias();\r\n\r\n\t\t\t\t\t\t\tList<String> column = config.getColumn();\r\n\t\t\t\t\t\t\tint mainColumnSize = column == null ? 0 : column.size();\r\n\t\t\t\t\t\t\tboolean toFindJoin = mainColumnSize <= 0 || i > mainColumnSize;  // 主表就不用找 JOIN 配置\r\n\r\n\t\t\t\t\t\t\tif (StringUtil.isEmpty(sqlTable, true)) {\r\n\t\t\t\t\t\t\t\t//sqlTable = null;\r\n\r\n\t\t\t\t\t\t\t\tif (toFindJoin) {  // 在主表字段数量内的都归属主表\r\n\t\t\t\t\t\t\t\t\tlong startTime3 = System.currentTimeMillis();\r\n\t\t\t\t\t\t\t\t\tsqlTable = rsmd.getTableName(i);  // SQL 函数甚至部分字段都不返回表名，当然如果没传 @column 生成的 Table.* 则返回的所有字段都会带表名\r\n\r\n                                    //if (StringUtil.isEmpty(sqlTable, true)) {\r\n\t\t\t\t\t\t\t\t\t//   boolean isEmpty = curItem == null || curItem.isEmpty();\r\n                                        String key = getKey(config, rs, rsmd, index, curItem, i, childMap, keyMap);\r\n\t\t\t\t\t\t\t\t\t\tchar last = repeatMap == null ? 0 : key.charAt(key.length() - 1);\r\n\t\t\t\t\t\t\t\t\t\tString repeatKey = last < '0' || last > '9' ? null : key.substring(0, key.length() - 1);\r\n\t\t\t\t\t\t\t\t\t\tInteger repeatCount = repeatKey == null ? null : repeatMap.get(repeatKey);\r\n\t\t\t\t\t\t\t\t\t\tint nc = repeatCount == null ? 1 : repeatCount + 1;\r\n\t\t\t\t\t\t\t\t\t\tif (last == nc + '0') {\r\n\t\t\t\t\t\t\t\t\t\t\tkeyMap.put(key, repeatKey);\r\n\t\t\t\t\t\t\t\t\t\t\trepeatMap.put(repeatKey, nc);\r\n\t\t\t\t\t\t\t\t\t\t\tkey = repeatKey; // QuestDB 会自动把副表与主表同名的字段重命名，例如 id 改为 id1, date 改为 date1\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\r\n                                        if (i > 1 && ( (curItem != null && curItem.containsKey(key))\r\n\t\t\t\t\t\t\t\t\t\t\t\t|| (StringUtil.isNotEmpty(key) && StringUtil.equals(key, curConfig == null ? null : curConfig.getIdKey())))\r\n\t\t\t\t\t\t\t\t\t\t) { // Presto 等引擎 JDBC 返回 rsmd.getTableName(i) 为空，主表如果一个字段都没有会导致 APISJON 主副表所有字段都不返回\r\n\t\t\t\t\t\t\t\t\t\t\tsqlTable = null;\r\n\t\t\t\t\t\t\t\t\t\t\tif (reseted) {\r\n\t\t\t\t\t\t\t\t\t\t\t\tSQLConfig<T, M, L> lastCfg = lastJoin == null ? null : lastJoin.getCacheConfig();\r\n\t\t\t\t\t\t\t\t\t\t\t\tList<String> lastColumn = lastCfg == null ? null : lastCfg.getColumn();\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\tlastViceTableStart ++;\r\n\t\t\t\t\t\t\t\t\t\t\t\tlastViceColumnStart += lastColumn == null ? 1 : lastColumn.size();\r\n\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\telse if (isMain) {\r\n\t\t\t\t\t\t\t\t\t\t\t\tfor (int j = 0; j < joinList.size(); j++) {\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tJoin<T, M, L> join = joinList.get(j);\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tSQLConfig<T, M, L> cfg = join == null || ! join.isSQLJoin() ? null : join.getJoinConfig();\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tList<String> c = cfg == null ? null : cfg.getColumn();\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (cfg != null) {\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsqlTable = cfg.gainSQLTable();\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsqlAlias = cfg.getAlias();\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlastViceTableStart = j;  // 避免后面的空 @column 表内字段被放到之前的空 @column 表\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tlastViceColumnStart = i + 1;\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcurJoin = join;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcurConfig = cfg;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcurColumn = c;\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttoFindJoin = false;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tisMain = false;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t\t\t\treseted = true;\r\n\t\t\t\t\t\t\t\t\t\t}\r\n                                    //}\r\n\t\t\t\t\t\t\t\t\tsqlResultDuration += System.currentTimeMillis() - startTime3;\r\n\r\n\t\t\t\t\t\t\t\t\tif (toFindJoin && StringUtil.isEmpty(sqlTable, true)) {  // hasJoin 已包含这个判断 && joinList != null) {\r\n\t\t\t\t\t\t\t\t\t\t//sqlTable = null; // QuestDB 等 rsmd.getTableName(i) 返回 \"\" 导致以下 StringUtil.equalsIgnoreCase 对比失败\r\n\r\n\t\t\t\t\t\t\t\t\t\tint nextViceColumnStart = lastViceColumnStart;  // 主表没有 @column 时会偏小 lastViceColumnStart\r\n\t\t\t\t\t\t\t\t\t\tint joinCount = joinList.size();\r\n\t\t\t\t\t\t\t\t\t\tfor (int j = lastViceTableStart; j < joinCount; j++) {  // 查找副表 @column，定位字段所在表\r\n\t\t\t\t\t\t\t\t\t\t\tJoin<T, M, L> join = joinList.get(j);\r\n\t\t\t\t\t\t\t\t\t\t\tSQLConfig<T, M, L> cfg = join == null || ! join.isSQLJoin() ? null : join.getJoinConfig();\r\n\t\t\t\t\t\t\t\t\t\t\tList<String> c = cfg == null ? null : cfg.getColumn();\r\n\r\n\t\t\t\t\t\t\t\t\t\t\tnextViceColumnStart += (c != null && ! c.isEmpty() ?\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tc.size() : (\r\n\t\t\t\t\t\t\t\t\t\t\t\t\tStringUtil.equalsIgnoreCase(sqlTable, lastTableName)\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t&& StringUtil.equals(sqlAlias, lastAliasName) ? 1 : 0\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t\t\t\t\t\tif (i < nextViceColumnStart) { // 导致只 JOIN 一张副表时主表数据放到副表 || j >= joinCount - 1) {\r\n\t\t\t\t\t\t\t\t\t\t\t\tsqlTable = cfg.gainSQLTable();\r\n\t\t\t\t\t\t\t\t\t\t\t\tsqlAlias = cfg.getAlias();\r\n\t\t\t\t\t\t\t\t\t\t\t\tlastViceTableStart = j;  // 避免后面的空 @column 表内字段被放到之前的空 @column 表\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\tcurJoin = join;\r\n\t\t\t\t\t\t\t\t\t\t\t\tcurConfig = cfg;\r\n\t\t\t\t\t\t\t\t\t\t\t\tcurColumn = c;\r\n\r\n\t\t\t\t\t\t\t\t\t\t\t\ttoFindJoin = false;\r\n\t\t\t\t\t\t\t\t\t\t\t\tisMain = false;\r\n\t\t\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t\t// 没有 @column，仍然定位不了，用前一个 table 名。FIXME 如果刚好某个表内第一个字段是就是 SQL 函数？\r\n\t\t\t\t\t\t\t\t\tif (StringUtil.isEmpty(sqlTable, true)) {\r\n\t\t\t\t\t\t\t\t\t\tsqlTable = lastTableName;\r\n\t\t\t\t\t\t\t\t\t\tsqlAlias = lastAliasName;\r\n\t\t\t\t\t\t\t\t\t\ttoFindJoin = false;\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t} else if (config.isClickHouse() && (sqlTable.startsWith(\"`\") || sqlTable.startsWith(\"\\\"\"))){\r\n\t\t\t\t\t\t\t\tsqlTable = sqlTable.substring(1, sqlTable.length() - 1);\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tif (StringUtil.equalsIgnoreCase(sqlTable, lastTableName) == false || StringUtil.equals(sqlAlias, lastAliasName) == false) {\r\n\t\t\t\t\t\t\t\tlastTableName = sqlTable;\r\n\t\t\t\t\t\t\t\tlastAliasName = sqlAlias;\r\n\t\t\t\t\t\t\t\tlastViceColumnStart = i;\r\n\r\n\t\t\t\t\t\t\t\tif (toFindJoin) {  // 找到对应的副表 JOIN 配置\r\n\t\t\t\t\t\t\t\t\tfor (int j = lastViceTableStart; j < joinList.size(); j++) {  // 查找副表 @column，定位字段所在表\r\n\t\t\t\t\t\t\t\t\t\tJoin join = joinList.get(j);\r\n\t\t\t\t\t\t\t\t\t\tSQLConfig<T, M, L> cfg = join == null || ! join.isSQLJoin() ? null : join.getJoinConfig();\r\n\r\n\t\t\t\t\t\t\t\t\t\tif (cfg != null && StringUtil.equalsIgnoreCase(sqlTable, cfg.gainSQLTable())\r\n\t\t\t\t\t\t\t\t\t\t) {  // FIXME 导致副表字段错放到主表 && StringUtil.equals(sqlAlias, cfg.getAlias())) {\r\n\t\t\t\t\t\t\t\t\t\t\tlastViceTableStart = j;  // 避免后面的空 @column 表内字段被放到之前的空 @column 表\r\n\r\n\t\t\t\t\t\t\t\t\t\t\tcurJoin = join;\r\n\t\t\t\t\t\t\t\t\t\t\tcurConfig = cfg;\r\n\t\t\t\t\t\t\t\t\t\t\tcurColumn = curConfig == null ? null : curConfig.getColumn();\r\n\r\n\t\t\t\t\t\t\t\t\t\t\tisMain = false;\r\n\t\t\t\t\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tif (isMain) {\r\n\t\t\t\t\t\t\t\tlastViceColumnStart ++;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\telse {\r\n\t\t\t\t\t\t\t\tif (curJoin == null) {\r\n\t\t\t\t\t\t\t\t\tcurJoin = lastJoin;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\telse {\r\n\t\t\t\t\t\t\t\t\tlastJoin = curJoin;\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\tif (curColumn == null) {\r\n\t\t\t\t\t\t\t\t\tcurConfig = curJoin == null || ! curJoin.isSQLJoin() ? null : curJoin.getJoinConfig();\r\n\t\t\t\t\t\t\t\t\tcurColumn = curConfig == null ? null : curConfig.getColumn();\r\n\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t// 解决后面的表内 SQL 函数被放到之前的空 @column 表\r\n\t\t\t\t\t\t\t\tif (curColumn == null || curColumn.isEmpty()) {\r\n\t\t\t\t\t\t\t\t\tlastViceColumnStart ++;\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tcolumnIndexAndJoinMap[i - 1] = curJoin;\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t// 如果是主表则直接用主表对应的 item，否则缓存副表数据到 childMap\r\n\t\t\t\t\t\tJoin prevJoin = columnIndexAndJoinMap == null || i < 2 ? null : columnIndexAndJoinMap[i - 2];\r\n\t\t\t\t\t\tif (curJoin != prevJoin) {  // 前后字段不在同一个表对象，即便后面出现 null，也不该是主表数据，而是逻辑 bug 导致\r\n\t\t\t\t\t\t\tSQLConfig<T, M, L> viceConfig = curJoin != null && curJoin.isSQLJoin() ? curJoin.getCacheConfig() : null;\r\n\t\t\t\t\t\t\tboolean hasPK = false;\r\n\t\t\t\t\t\t\tif (viceConfig != null) {  //FIXME 只有和主表关联才能用 item，否则应该从 childMap 查其它副表数据\r\n\t\t\t\t\t\t\t\tList<On> onList = curJoin.getOnList();\r\n\t\t\t\t\t\t\t\tint size = onList == null ? 0 : onList.size();\r\n\t\t\t\t\t\t\t\tif (size > 0) {\r\n\t\t\t\t\t\t\t\t\tString idKey = viceConfig.getIdKey();\r\n\t\t\t\t\t\t\t\t\tString tblKey = config.gainTableKey();\r\n\t\t\t\t\t\t\t\t\tfor (int j = size - 1; j >= 0; j--) {\r\n\t\t\t\t\t\t\t\t\t\tOn on = onList.get(j);\r\n\t\t\t\t\t\t\t\t\t\tString ok = on == null ? null : on.getOriginKey();\r\n\t\t\t\t\t\t\t\t\t\tif (ok == null) {\r\n\t\t\t\t\t\t\t\t\t\t\tthrow new NullPointerException(\"服务器内部错误，List<Join> 中 Join.onList[\" + j + (on == null ? \"] = null！\" : \".getOriginKey() = null！\"));\r\n\t\t\t\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\t\t\t\tString k = ok.substring(0, ok.length() - 1);\r\n\t\t\t\t\t\t\t\t\t\tString ttk = on.getTargetTableKey();\r\n\r\n\t\t\t\t\t\t\t\t\t\tM target = StringUtil.equals(ttk, tblKey) ? item : (viceItem == null ? null : JSON.get(viceItem, ttk));\r\n\t\t\t\t\t\t\t\t\t\tObject v = target == null ? null : target.get(on.getTargetKey());\r\n\t\t\t\t\t\t\t\t\t\thasPK = hasPK || (k.equals(idKey) && v != null);\r\n\r\n\t\t\t\t\t\t\t\t\t\tviceConfig.putWhere(k, v, true);\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t\tif (viceConfig == null) { // StringUtil.isEmpty(viceSql, true)) {\r\n\t\t\t\t\t\t\t\tLog.i(TAG, \"execute viceConfig == null >> item = null; >> \");\r\n\t\t\t\t\t\t\t\tcurItem = null;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\telse if (curJoin.isOuterJoin() || curJoin.isAntiJoin()) {\r\n\t\t\t\t\t\t\t\tLog.i(TAG, \"execute curJoin.isOuterJoin() || curJoin.isAntiJoin() >> item = null; >> \");\r\n\t\t\t\t\t\t\t\tcurItem = null;  // 肯定没有数据，缓存也无意义\r\n\t\t\t\t\t\t\t\t// 副表是按常规条件查询，缓存会导致其它同表同条件对象查询结果集为空\tchildMap.put(viceSql, JSON.createJSONObject());  // 缓存固定空数据，避免后续多余查询\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\telse {\r\n\t\t\t\t\t\t\t\tString viceName = viceConfig.gainTableKey();\r\n\t\t\t\t\t\t\t\tif (viceItem == null) {\r\n\t\t\t\t\t\t\t\t\tviceItem = JSON.createJSONObject();\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\tcurItem = JSON.get(viceItem, viceName);\r\n\r\n\t\t\t\t\t\t\t\tString viceSql = hasPK ? viceConfig.gainSQL(false) : null; // TODO 在 SQLConfig<T, M, L> 缓存 SQL，减少大量的重复生成\r\n\t\t\t\t\t\t\t\tM curCache = hasPK ? childMap.get(viceSql) : null;\r\n\r\n\t\t\t\t\t\t\t\tif (curItem == null || curItem.isEmpty()) {\r\n\t\t\t\t\t\t\t\t\t// 导致前面判断重复 key 出错 curItem = curCache != null ? curCache : JSON.createJSONObject();\r\n\t\t\t\t\t\t\t\t\tcurItem = JSON.createJSONObject();\r\n\t\t\t\t\t\t\t\t\tviceItem.put(viceName, curItem);\r\n\t\t\t\t\t\t\t\t\tif (hasPK && curCache == null) {\r\n\t\t\t\t\t\t\t\t\t\tchildMap.put(viceSql, curItem);\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\telse if (hasPK) {\r\n\t\t\t\t\t\t\t\t\tif (curCache == null || curCache.isEmpty()) {\r\n\t\t\t\t\t\t\t\t\t\tchildMap.put(viceSql, curItem);\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\telse {\r\n\t\t\t\t\t\t\t\t\t\tcurCache.putAll(curItem);\r\n\t\t\t\t\t\t\t\t\t\t// 导致前面判断重复 key 出错 curItem = curCache;\r\n\t\t\t\t\t\t\t\t\t\t// viceItem.put(viceName, curItem);\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tcurItem = (M) onPutColumn(config, rs, rsmd, index, curItem, i, curJoin, childMap, keyMap);  // isExplain == false && hasJoin && i >= viceColumnStart ? childMap : null);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (viceItem != null) {\r\n\t\t\t\t\t\titem.put(KEY_VICE_ITEM, viceItem);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tresultList = onPutTable(config, rs, rsmd, resultList, index, item);\r\n\r\n\t\t\t\t\tLog.d(TAG, \"execute  while (rs.next()) { resultList.put( \" + index + \", result); \" + \"\\n >>>>>>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\tfinally {\r\n\t\t\tif (rs != null) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\trs.close();\r\n\t\t\t\t}\r\n\t\t\t\tcatch (Exception e) {\r\n\t\t\t\t\tLog.e(TAG, \"close ResultSet failed\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (resultList == null) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif (unknownType || isExplain) {\r\n\t\t\tif (isExplain) {\r\n\t\t\t\tif (result == null) {\r\n\t\t\t\t\tresult = JSON.createJSONObject();\r\n\t\t\t\t}\r\n\t\t\t\tconfig.setExplain(false);\r\n\t\t\t\tresult.put(\"sql\", config.gainSQL(false));\r\n\t\t\t\tconfig.setExplain(isExplain);\r\n\t\t\t}\r\n\t\t\tresult.put(\"list\", resultList);\r\n\r\n\t\t\tif (unknownType == false) {\r\n\t\t\t\tputCache(sql, Arrays.asList(result), config);\r\n\t\t\t}\r\n\r\n\t\t\treturn result;\r\n\t\t}\r\n\r\n\t\tif (isHead == false) {\r\n\t\t\t// @ APP JOIN 查询副表并缓存到 childMap <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\r\n\t\t\tMap<String, List<M>> appJoinChildMap = new HashMap<>();\r\n\t\t\tchildMap.forEach((viceSql, item) -> appJoinChildMap.put(viceSql, Arrays.asList(item)));\r\n\t\t\texecuteAppJoin(config, resultList, appJoinChildMap, keyMap);\r\n\r\n\t\t\t// @ APP JOIN 查询副表并缓存到 childMap >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n\r\n\t\t\t//子查询 SELECT Moment.*, Comment.id 中的 Comment 内字段\r\n\t\t\tSet<Entry<String, List<M>>> set = appJoinChildMap.entrySet();\r\n\r\n\t\t\t//<sql, Table>\r\n\t\t\tfor (Entry<String, List<M>> entry : set) {\r\n\t\t\t\tputCache(entry.getKey(), entry.getValue(), null);\r\n\t\t\t}\r\n\r\n\t\t\tLog.i(TAG, \">>> execute  putCache('\" + sql + \"', resultList);  resultList.size() = \" + resultList.size());\r\n\r\n\t\t\t// 数组主表对象额外一次返回全部，方便 Parser<T, M, L> 缓存来提高性能\r\n\r\n\t\t\tresult = position >= resultList.size() ? JSON.createJSONObject() : resultList.get(position);\r\n\t\t\tif (position == 0 && resultList.size() > 1 && result != null && result.isEmpty() == false) {\r\n\t\t\t\t// 不是 main 不会直接执行，count=1 返回的不会超过 1   && config.isMain() && config.getCount() != 1\r\n\t\t\t\tLog.i(TAG, \">>> execute  position == 0 && resultList.size() > 1 && result != null && result.isEmpty() == false\"\r\n\t\t\t\t\t\t+ \" >> result = JSON.createJSONObject(result); result.put(KEY_RAW_LIST, resultList);\");\r\n\r\n\t\t\t\tresult = (M) JSON.createJSONObject(result);\r\n\t\t\t\tresult.put(KEY_RAW_LIST, resultList);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tputCache(sql, resultList, config);\r\n\r\n\t\tlong endTime = System.currentTimeMillis();\r\n\t\tLog.d(TAG, \"\\n\\n execute  endTime = \" + endTime + \"; duration = \" + (endTime - startTime)\r\n\t\t\t\t+ \"\\n return resultList.get(\" + position + \");\"  + \"\\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\\n\");\r\n\t\treturn result;\r\n\t}\r\n\r\n\r\n\t/**@ APP JOIN 查询副表并缓存到 childMap\r\n\t * @param config\r\n\t * @param resultList\r\n\t * @param childMap\r\n\t * @throws Exception\r\n\t */\r\n\tprotected void executeAppJoin(SQLConfig<T, M, L> config, List<M> resultList, Map<String, List<M>> childMap, Map<String, String> keyMap) throws Exception {\r\n\t\tList<Join<T, M, L>> joinList = config.getJoinList();\r\n\t\tif (joinList != null) {\r\n\r\n\t\t\tfor (Join<T, M, L> join : joinList) {\r\n\t\t\t\tif (join.isAppJoin() == false) {\r\n\t\t\t\t\tLog.i(TAG, \"executeAppJoin  for (Join j : joinList) >> j.isAppJoin() == false >>  continue;\");\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tSQLConfig<T, M, L> cc = join.getCacheConfig(); //这里用config改了getSQL后再还原很麻烦，所以提前给一个config2更好\r\n\t\t\t\tif (cc == null) {\r\n\t\t\t\t\tif (Log.DEBUG) {\r\n\t\t\t\t\t\tthrow new NullPointerException(\"服务器内部错误, executeAppJoin cc == null ! 导致不能缓存 @ APP JOIN 的副表数据！\");\r\n\t\t\t\t\t}\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tSQLConfig<T, M, L> jc = join.getJoinConfig();\r\n\r\n\t\t\t\tList<On> onList = join.getOnList();\r\n\t\t\t\tOn on = onList == null || onList.isEmpty() ? null : onList.get(0);  // APP JOIN 应该有且只有一个 ON 条件\r\n\t\t\t\tString originKey = on == null ? null : on.getOriginKey();\r\n\t\t\t\tif (originKey == null) {\r\n\t\t\t\t\tthrow new NullPointerException(\"服务器内部错误，List<Join> 中 Join.onList[0\" + (on == null ? \"] = null！\" : \".getOriginKey() = null！\"));\r\n\t\t\t\t}\r\n\t\t\t\tString key = on.getKey();\r\n\t\t\t\tif (key == null) {\r\n\t\t\t\t\tthrow new NullPointerException(\"服务器内部错误，List<Join> 中 Join.onList[0\" + (on == null ? \"] = null！\" : \".getKey() = null！\"));\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// 取出 \"id@\": \"@/User/userId\" 中所有 userId 的值\r\n\t\t\t\tList<Object> targetValueList = new ArrayList<>();\r\n\r\n\t\t\t\tfor (int i = 0; i < resultList.size(); i++) {\r\n\t\t\t\t\tM mainTable = resultList.get(i);\r\n\t\t\t\t\tObject targetValue = mainTable == null ? null : mainTable.get(on.getTargetKey());\r\n\r\n\t\t\t\t\tif (targetValue != null && targetValueList.contains(targetValue) == false) {\r\n\t\t\t\t\t\ttargetValueList.add(targetValue);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (targetValueList.isEmpty() && config.isExplain() == false) {\r\n\t\t\t\t\tthrow new NotExistException(\"targetValueList.isEmpty() && config.isExplain() == false\");\r\n\t\t\t\t}\r\n\r\n\t\t\t\t// 替换为 \"id{}\": [userId1, userId2, userId3...]\r\n\t\t\t\tjc.putWhere(originKey, null, false);  // remove originKey\r\n\t\t\t\tjc.putWhere(key + \"{}\", targetValueList, true);  // add originKey{}          }\r\n\r\n                jc.setMain(true).setPreparedValueList(new ArrayList<>());\r\n\r\n\t\t\t\t// 放一块逻辑更清晰，也避免解析 * 等不支持或性能开销\r\n\t\t\t\t//        String q = jc.getQuote();\r\n\t\t\t\t//        if (allChildCount > 0 && jc.getCount() <= 0) {\r\n\t\t\t\t//          List<String> column = jc.getColumn();\r\n\t\t\t\t//          if (column == null || column.isEmpty()) {\r\n\t\t\t\t//            column = Arrays.asList(\"*;row_number()OVER(PARTITION BY \" + q + key + q + \" ORDER BY \" + q + key + q + \" ASC):_row_num_\");\r\n\t\t\t\t//          }\r\n\t\t\t\t//          else {\r\n\t\t\t\t//            column.add(\"row_number()OVER(PARTITION BY \" + q + key + q + \" ORDER BY \" + q + key + q + \" ASC):_row_num_\");\r\n\t\t\t\t//          }\r\n\t\t\t\t//          jc.setColumn(column);\r\n\t\t\t\t//        }\r\n\r\n\t\t\t\tint childCount = cc.getCount();\r\n\t\t\t\tint allChildCount = childCount*config.getCount();  // 所有分组子项数量总和\r\n\t\t\t\tboolean isOne2Many = childCount != 1 || join.isOne2Many();\r\n\t\t\t\t// 一对多会漏副表数据  TODO 似乎一对一走以下优化 row_number() <= childCount 逻辑也没问题\r\n\t\t\t\t//        if (isOne2Many == false && allChildCount > 0 && jc.getCount() < allChildCount) {\r\n\t\t\t\t//          jc.setCount(allChildCount);\r\n\t\t\t\t//        }\r\n\r\n\t\t\t\tboolean prepared = jc.isPrepared();\r\n\t\t\t\tString sql = jc.gainSQL(false);\r\n\r\n\t\t\t\tif (StringUtil.isEmpty(sql, true)) {\r\n\t\t\t\t\tthrow new NullPointerException(TAG + \".executeAppJoin  StringUtil.isEmpty(sql, true) >> return null;\");\r\n\t\t\t\t}\r\n\r\n\t\t\t\tString sql2 = null;\r\n\t\t\t\tif (childCount > 0 && isOne2Many && (jc.isMySQL() == false || jc.gainDBVersionNums()[0] >= 8)) {\r\n\t\t\t\t\t//          加 row_number 字段并不会导致 count 等聚合函数统计出错，结果偏大，SQL JOIN 才会，之前没发现是因为缓存失效 bug\r\n\t\t\t\t\t//          boolean noAggrFun = true;\r\n\t\t\t\t\t//          List<String> column = jc.getColumn();\r\n\t\t\t\t\t//          if (column != null) {\r\n\t\t\t\t\t//            for (String c : column) {\r\n\t\t\t\t\t//              int start = c == null ? -1 : c.indexOf(\"(\");\r\n\t\t\t\t\t//              int end = start <= 0 ? -1 : c.lastIndexOf(\")\");\r\n\t\t\t\t\t//              if (start > 0 && end > start) {\r\n\t\t\t\t\t//                String fun = c.substring(0, start);\r\n\t\t\t\t\t//                if (AbstractSQLConfig.SQL_AGGREGATE_FUNCTION_MAP.containsKey(fun)) {\r\n\t\t\t\t\t//                  noAggrFun = false;\r\n\t\t\t\t\t//                  break;\r\n\t\t\t\t\t//                }\r\n\t\t\t\t\t//              }\r\n\t\t\t\t\t//            }\r\n\t\t\t\t\t//          }\r\n\t\t\t\t\t//\r\n\t\t\t\t\t//          if (noAggrFun) { // 加 row_number 字段会导致 count 等聚合函数统计出错，结果偏大？\r\n\t\t\t\t\tString q = jc.getQuote();\r\n\t\t\t\t\tsql2 = prepared && jc.isTDengine() == false ? jc.gainSQL(true) : sql;\r\n\r\n\t\t\t\t\tString prefix = \"SELECT * FROM(\";\r\n\t\t\t\t\tString rnStr = \", row_number() OVER (PARTITION BY \" + q + key + q + ((AbstractSQLConfig) jc).gainOrderString(true) + \") _row_num_ FROM \";\r\n\t\t\t\t\tString suffix = \") _t WHERE ( (_row_num_ <= \" + childCount + \") )\" + (allChildCount > 0 ? \" LIMIT \" + allChildCount : \"\");\r\n\r\n\t\t\t\t\tsql2 = prefix\r\n\t\t\t\t\t\t\t// 放一块逻辑更清晰，也避免解析 * 等不支持或性能开销  + sql\r\n\t\t\t\t\t\t\t+ sql2.replaceFirst(\" FROM \", rnStr)  // * 居然只能放在 row_number() 前面，放后面就报错 \"SELECT \", rnStr)\r\n\t\t\t\t\t\t\t+ suffix;\r\n\r\n\t\t\t\t\tsql = prepared ? (prefix + sql.replaceFirst(\" FROM \", rnStr) + suffix) : sql2;\r\n\t\t\t\t\t//          }\r\n\t\t\t\t}\r\n\r\n\t\t\t\tboolean isExplain = jc.isExplain();\r\n\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\tgeneratedSQLCount ++;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tlong startTime = System.currentTimeMillis();\r\n\t\t\t\tLog.d(TAG, \"\\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\"\r\n\t\t\t\t\t\t+ \"\\n executeAppJoin  startTime = \" + startTime\r\n\t\t\t\t\t\t+ \"\\n sql = \\n \" + sql\r\n\t\t\t\t\t\t+ \"\\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\");\r\n\r\n\t\t\t\t//执行副表的批量查询 并 缓存到 childMap\r\n\t\t\t\tResultSet rs = null;\r\n\t\t\t\ttry {\r\n\t\t\t\t\tlong executedSQLStartTime = 0;\r\n\t\t\t\t\tif (isExplain == false) { //只有 SELECT 才能 EXPLAIN\r\n\t\t\t\t\t\texecutedSQLCount ++;\r\n\t\t\t\t\t\texecutedSQLStartTime = System.currentTimeMillis();\r\n\t\t\t\t\t}\r\n                    rs = executeQuery(jc, sql2);\r\n\t\t\t\t\tif (isExplain == false) {\r\n\t\t\t\t\t\texecutedSQLDuration += System.currentTimeMillis() - executedSQLStartTime;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tint count = 0;\r\n\r\n\t\t\t\t\tint index = -1;\r\n\r\n\t\t\t\t\tlong startTime2 = System.currentTimeMillis();\r\n\t\t\t\t\tResultSetMetaData rsmd = rs.getMetaData();\r\n\t\t\t\t\tfinal int length = rsmd.getColumnCount();\r\n\t\t\t\t\tsqlResultDuration += System.currentTimeMillis() - startTime2;\r\n\r\n\t\t\t\t\tMap<String, Boolean> skipMap = new HashMap<>();\r\n\r\n\t\t\t\t\tlong lastCursorTime = System.currentTimeMillis();\r\n\t\t\t\t\twhile ((allChildCount <= 0 || count < allChildCount) && rs.next()) { //FIXME 同时有 @ APP JOIN 和 < 等 SQL JOIN 时，next = false 总是无法进入循环，导致缓存失效，可能是连接池或线程问题\r\n\t\t\t\t\t\tsqlResultDuration += System.currentTimeMillis() - lastCursorTime;\r\n\t\t\t\t\t\tlastCursorTime = System.currentTimeMillis();\r\n\r\n\t\t\t\t\t\tindex ++;\r\n\t\t\t\t\t\tLog.d(TAG, \"\\n\\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\\n executeAppJoin while (rs.next()){  index = \" + index + \"\\n\\n\");\r\n\r\n\t\t\t\t\t\tM result = JSON.createJSONObject();\r\n\r\n\t\t\t\t\t\tfor (int i = 1; i <= length; i++) {\r\n\t\t\t\t\t\t\tresult = onPutColumn(jc, rs, rsmd, index, result, i, null, null, keyMap);\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\t//每个 result 都要用新的 SQL 来存 childResultMap = onPutTable(config, rs, rsmd, childResultMap, index, result);\r\n\r\n\t\t\t\t\t\tLog.d(TAG, \"\\n executeAppJoin  while (rs.next()) { resultList.put(\" + index + \", result); \"\r\n\t\t\t\t\t\t\t\t+ \"\\n >>>>>>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\r\n\t\t\t\t\t\t//TODO 兼容复杂关联\r\n\t\t\t\t\t\tcc.putWhere(key, result.get(key), true);  // APP JOIN 应该有且只有一个 ON 条件\r\n\t\t\t\t\t\tString cacheSql = cc.gainSQL(false);\r\n\t\t\t\t\t\tList<M> results = childMap.get(cacheSql);\r\n\r\n\t\t\t\t\t\tif (results == null || skipMap.get(cacheSql) == null) {  // 避免添加重复数据\r\n\t\t\t\t\t\t\tresults = new ArrayList<>(childCount);\r\n\t\t\t\t\t\t\tchildMap.put(cacheSql, results);\r\n\t\t\t\t\t\t\tskipMap.put(cacheSql, Boolean.TRUE);\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tif (childCount <= 0 || results.size() < childCount) {  // 避免超过子数组每页数量\r\n\t\t\t\t\t\t\t//              if (count == 1 && results.isEmpty() == false) {  // 避免添加重复数据\r\n\t\t\t\t\t\t\t//                results.clear();\r\n\t\t\t\t\t\t\t//              }\r\n\t\t\t\t\t\t\tresults.add(result);  //缓存到 childMap\r\n\t\t\t\t\t\t\tcount ++;\r\n\t\t\t\t\t\t\tLog.d(TAG, \">>> executeAppJoin childMap.put('\" + cacheSql + \"', result);  childMap.size() = \" + childMap.size());\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tfinally {\r\n\t\t\t\t\tif (rs != null) {\r\n\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\trs.close();\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tcatch (Exception e) {\r\n\t\t\t\t\t\t\tLog.e(TAG, \"close ResultSet failed in executeAppJoin\", e);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tlong endTime = System.currentTimeMillis();\r\n\t\t\t\tLog.d(TAG, \"\\n\\n executeAppJoin  endTime = \" + endTime + \"; duration = \" + (endTime - startTime)\r\n\t\t\t\t\t\t+ \"\\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\\n\\n\");\r\n\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t}\r\n\r\n\r\n\r\n\t/**table.put(rsmd.getColumnLabel(i), rs.getObject(i));\r\n\t * @param config\r\n\t * @param rs\r\n\t * @param rsmd\r\n\t * @param row 从0开始\r\n\t * @param table\r\n\t * @param columnIndex 从1开始\r\n\t * @param childMap\r\n\t * @return result\r\n\t * @throws Exception\r\n\t */\r\n\tprotected M onPutColumn(@NotNull SQLConfig<T, M, L> config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd\r\n\t\t\t, final int row, @NotNull M table, final int columnIndex, Join<T, M, L> join, Map<String, M> childMap\r\n\t\t\t, Map<String, String> keyMap) throws Exception {\r\n\t\tif (table == null) {  // 对应副表 viceSql 不能生成正常 SQL， 或者是 ! - Outer, ( - ANTI JOIN 的副表这种不需要缓存及返回的数据\r\n\t\t\tLog.i(TAG, \"onPutColumn table == null >> return table;\");\r\n\t\t\treturn table;\r\n\t\t}\r\n\r\n\t\tif (isHideColumn(config, rs, rsmd, row, table, columnIndex, childMap, keyMap)) {\r\n\t\t\tLog.i(TAG, \"onPutColumn isHideColumn(config, rs, rsmd, row, table, columnIndex, childMap) >> return table;\");\r\n\t\t\treturn table;\r\n\t\t}\r\n\r\n\t\tString label = getKey(config, rs, rsmd, row, table, columnIndex, childMap, keyMap);\r\n\t\tObject value = getValue(config, rs, rsmd, row, table, columnIndex, label, childMap, keyMap);\r\n\r\n\t\t// 主表必须 put 至少一个 null 进去，否则全部字段为 null 都不 put 会导致中断后续正常返回值\r\n\t\tif (value != null || ENABLE_OUTPUT_NULL_COLUMN || (join == null && table.isEmpty())) {\r\n\t\t\ttable.put(label, value);\r\n\t\t}\r\n\r\n\t\treturn table;\r\n\t}\r\n\r\n\t/**如果不需要这个功能，在子类重写并直接 return false; 来提高性能\r\n\t * @param config\r\n\t * @param rs\r\n\t * @param rsmd\r\n\t * @param row\r\n\t * @param table\r\n\t * @param columnIndex\r\n\t * @param childMap\r\n\t * @return\r\n\t * @throws SQLException\r\n\t */\r\n\tprotected boolean isHideColumn(@NotNull SQLConfig<T, M, L> config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd\r\n\t\t\t, final int row, @NotNull M table, final int columnIndex, Map<String, M> childMap\r\n\t\t\t, Map<String, String> keyMap) throws SQLException {\r\n\t\treturn rsmd.getColumnName(columnIndex).startsWith(\"_\");\r\n\t}\r\n\r\n\t/**resultList.put(position, table);\r\n\t * @param config\r\n\t * @param rs\r\n\t * @param rsmd\r\n\t * @param resultList\r\n\t * @param position\r\n\t * @param table\r\n\t * @return resultList\r\n\t */\r\n\tprotected List<M> onPutTable(@NotNull SQLConfig<T, M, L> config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd\r\n\t\t\t, @NotNull List<M> resultList, int position, @NotNull M table) {\r\n\r\n\t\tresultList.add(table);\r\n\t\treturn resultList;\r\n\t}\r\n\r\n\tprotected String getKey(@NotNull SQLConfig<T, M, L> config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd\r\n\t\t\t, final int row, @NotNull M table, final int columnIndex, Map<String, M> childMap\r\n\t\t\t, Map<String, String> keyMap) throws Exception {\r\n\t\tlong startTime = System.currentTimeMillis();\r\n\t\tString key = rsmd.getColumnLabel(columnIndex);  // dotIndex < 0 ? label : label.substring(dotIndex + 1);\r\n\t\tsqlResultDuration += System.currentTimeMillis() - startTime;\r\n\r\n\t\tif (config.isHive()) {\r\n\t\t\tString tableName = config.getTable();\r\n\t\t\tString realTableName = AbstractSQLConfig.TABLE_KEY_MAP.get(tableName);\r\n\r\n\t\t\tString pattern = \"^\" + (StringUtil.isEmpty(realTableName, true) ? tableName : realTableName) + \"\\\\.\" + \"[a-zA-Z]+$\";\r\n\t\t\tboolean isMatch = Pattern.matches(pattern, key);\r\n\t\t\tif (isMatch) {\r\n\t\t\t\tkey = key.split(\"\\\\.\")[1];\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (keyMap != null && ! keyMap.isEmpty()) {\r\n\t\t\tString nk = keyMap.get(key);\r\n\t\t\tif (StringUtil.isNotEmpty(nk, true)) {\r\n\t\t\t\tkey = nk; // QuestDB 会自动把副表与主表同名的字段重命名，例如 id 改为 id1, date 改为 date1\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn key;\r\n\t}\r\n\r\n\tprotected Object getValue(@NotNull SQLConfig<T, M, L> config, @NotNull ResultSet rs, @NotNull ResultSetMetaData rsmd\r\n\t\t\t, final int row, @NotNull M table, final int columnIndex, String label\r\n\t\t\t, Map<String, M> childMap, Map<String, String> keyMap) throws Exception {\r\n\r\n\t\tlong startTime = System.currentTimeMillis();\r\n\t\tObject value = rs.getObject(columnIndex);\r\n\t\tsqlResultDuration += System.currentTimeMillis() - startTime;\r\n\r\n\t\t//\t\t\t\t\tLog.d(TAG, \"name:\" + rsmd.getColumnName(i));\r\n\t\t//\t\t\t\t\tLog.d(TAG, \"label:\" + rsmd.getColumnLabel(i));\r\n\t\t//\t\t\t\t\tLog.d(TAG, \"type:\" + rsmd.getColumnType(i));\r\n\t\t//\t\t\t\t\tLog.d(TAG, \"typeName:\" + rsmd.getColumnTypeName(i));\r\n\r\n\t\t//\t\t\t\tLog.i(TAG, \"select  while (rs.next()) { >> for (int i = 0; i < length; i++) {\"\r\n\t\t//\t\t\t\t\t\t+ \"\\n  >>> value = \" + value);\r\n\r\n\t\tboolean castToJson = false;\r\n\r\n\t\t//数据库查出来的null和empty值都有意义，去掉会导致 Moment:{ @column:\"content\" } 部分无结果及中断数组查询！\r\n\t\tif (value instanceof Boolean) {\r\n\t\t\t//加快判断速度\r\n\t\t}\r\n\t\telse if (value instanceof Number) {\r\n\t\t\tvalue = getNumVal((Number) value);\r\n\t\t}\r\n\t\telse if (value instanceof Timestamp) {\r\n\t\t\tvalue = ((Timestamp) value).toString();\r\n\t\t}\r\n\t\telse if (value instanceof Date) {  // java.sql.Date 和 java.sql.Time 都继承 java.util.Date\r\n\t\t\tvalue = ((Date) value).toString();\r\n\t\t}\r\n\t\telse if (value instanceof LocalDateTime) {\r\n\t\t\tvalue = ((LocalDateTime) value).toString();\r\n\t\t}\r\n\t\telse if (value instanceof Year) {\r\n\t\t\tvalue = ((Year) value).getValue();\r\n\t\t}\r\n\t\telse if (value instanceof Month) {\r\n\t\t\tvalue = ((Month) value).getValue();\r\n\t\t}\r\n\t\telse if (value instanceof DayOfWeek) {\r\n\t\t\tvalue = ((DayOfWeek) value).getValue();\r\n\t\t}\r\n\t\telse if (value instanceof String && isJSONType(config, rsmd, columnIndex, label)) { //json String\r\n\t\t\tcastToJson = true;\r\n\t\t}\r\n\t\telse if (value instanceof Blob) { //FIXME 存的是 abcde，取出来直接就是 [97, 98, 99, 100, 101] 这种 byte[] 类型，没有经过以下处理，但最终序列化后又变成了字符串 YWJjZGU=\r\n\t\t\tcastToJson = true;\r\n\t\t\tvalue = new String(((Blob) value).getBytes(1, (int) ((Blob) value).length()), \"UTF-8\");\r\n\t\t}\r\n\t\telse if (value instanceof Clob) { //SQL Server TEXT 类型 居然走这个\r\n\t\t\tcastToJson = true;\r\n\r\n\t\t\tStringBuffer sb = new StringBuffer();\r\n\t\t\tBufferedReader br = new BufferedReader(((Clob) value).getCharacterStream());\r\n\t\t\tString s = br.readLine();\r\n\t\t\twhile (s != null) {\r\n\t\t\t\tsb.append(s);\r\n\t\t\t\ts = br.readLine();\r\n\t\t\t}\r\n\t\t\tvalue = sb.toString();\r\n\r\n\t\t\ttry {\r\n\t\t\t\tbr.close();\r\n\t\t\t}\r\n\t\t\tcatch (Exception e) {\r\n\t\t\t\tLog.e(TAG, \"close BufferedReader failed\", e);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (castToJson == false) {\r\n\t\t\tList<String> json = config.getJson();\r\n\t\t\tcastToJson = json != null && json.contains(label);\r\n\t\t}\r\n\t\tif (castToJson) {\r\n\t\t\ttry {\r\n\t\t\t\tvalue = JSON.parse(value);\r\n\t\t\t} catch (Exception e) {\r\n\t\t\t\tLog.e(TAG, \"getValue  try { value = parseJSON((String) value); } catch (Exception e) { \\n\" + e.getMessage());\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn value;\r\n\t}\r\n\r\n\tpublic Object getNumVal(Number value) {\r\n\t\tif (value == null) {\r\n\t\t\treturn null;\r\n\t\t}\r\n\r\n\t\tif (value instanceof BigInteger) {\r\n\t\t\treturn ((BigInteger) value).toString();\r\n\t\t}\r\n\r\n\t\tif (value instanceof BigDecimal) {\r\n\t\t\treturn ((BigDecimal) value).toString();\r\n\t\t}\r\n\r\n\t\tdouble v = value.doubleValue();\r\n\t\t// if (v > Integer.MAX_VALUE || v < Integer.MIN_VALUE) { // 避免前端/客户端拿到精度丢失甚至严重失真的值\r\n\t\t// \treturn value.toString();\r\n\t\t// }\r\n\t\t// JavaScript: Number.MAX_SAFE_INTEGER ~ Number.MIN_SAFE_INTEGER\r\n\t\tif (v > 9007199254740991L || v < -9007199254740991L) { // 避免前端/客户端拿到精度丢失甚至严重失真的值\r\n\t\t\treturn value.toString();\r\n\t\t}\r\n\t\t\r\n\t\treturn value;\r\n\t}\r\n\r\n\r\n\t/**判断是否为JSON类型\r\n\t * @param config\r\n\t * @param label\r\n\t * @param rsmd\r\n\t * @param position\r\n\t * @return\r\n\t */\r\n\t@Override\r\n\tpublic boolean isJSONType(@NotNull SQLConfig<T, M, L> config, ResultSetMetaData rsmd, int position, String label) {\r\n\t\ttry {\r\n\t\t\tlong startTime = System.currentTimeMillis();\r\n\t\t\tString column = rsmd.getColumnTypeName(position);\r\n\t\t\tsqlResultDuration += System.currentTimeMillis() - startTime;\r\n\t\t\t// nosql elasticSearch jdbc获取不到 字段类型\r\n\t\t\tif(StringUtil.isEmpty(column)) {\r\n\t\t\t\treturn false;\r\n\t\t\t}\r\n\t\t\t//TODO CHAR和JSON类型的字段，getColumnType返回值都是1\t，如果不用CHAR，改用VARCHAR，则可以用上面这行来提高性能。\r\n\t\t\t//return rsmd.getColumnType(position) == 1;\r\n\r\n\t\t\tif (column.toLowerCase().contains(\"json\")) {\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t} catch (SQLException e) {\r\n\t\t\tLog.e(TAG, \"isJsonColumn failed\", e);\r\n\t\t}\r\n\t\t//\t\tList<String> json = config.getJson();\r\n\t\t//\t\treturn json != null && json.contains(label);\r\n\t\treturn false;\r\n\t}\r\n\r\n\r\n\t@Override  // 重写是为了返回类型从 Statement 改为 PreparedStatement，避免其它方法出错\r\n\tpublic PreparedStatement getStatement(@NotNull SQLConfig<T, M, L> config) throws Exception {\r\n\t\treturn getStatement(config, null);\r\n\t}\r\n\r\n\t@Override\r\n\tpublic PreparedStatement getStatement(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception {\r\n\t\tif (StringUtil.isEmpty(sql)) {\r\n\t\t\tsql = config.gainSQL(config.isPrepared());\r\n\t\t}\r\n\r\n\t\tConnection conn = getConnection(config);\r\n\t\tPreparedStatement statement; //创建Statement对象\r\n\t\tif (config.getMethod() == RequestMethod.POST && config.getId() == null) { //自增id\r\n\t\t\tif (config.isOracle()) {\r\n\t\t\t\t// 解决 oracle 使用自增主键 插入获取不到id问题\r\n\t\t\t\tString[] generatedColumns = {config.getIdKey()};\r\n\t\t\t\tstatement = conn.prepareStatement(sql, generatedColumns);\r\n\t\t\t} else {\r\n\t\t\t\tstatement = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);\r\n\t\t\t}\r\n\t\t}\r\n\t\telse if (RequestMethod.isGetMethod(config.getMethod(), true)) {\r\n            // if (config.isPresto() || config.isTrino()) {\r\n            //    statement = getConnection(config).prepareStatement(sql); // , ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);\r\n            // } else {\r\n            //    statement = getConnection(config).prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);\r\n            // }\r\n\r\n\t\t\t// TODO 补充各种支持 TYPE_SCROLL_SENSITIVE 和 CONCUR_UPDATABLE 的数据库\r\n            if (config.isMySQL() || config.isTiDB() || config.isMariaDB() || config.isOracle() || config.isSQLServer() || config.isDb2()\r\n\t\t\t\t\t|| config.isPostgreSQL() || config.isCockroachDB() || config.isOpenGauss() || config.isTimescaleDB() || config.isQuestDB()\r\n\t\t\t) {\r\n                statement = conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);\r\n            } else {\r\n                statement = conn.prepareStatement(sql);\r\n            }\r\n\t\t}\r\n\t\telse {\r\n\t\t\tstatement = conn.prepareStatement(sql);\r\n\t\t}\r\n\r\n\t\tList<Object> valueList = config.isPrepared() ? config.getPreparedValueList() : null;\r\n//\t\tList<Object> withAsExprePreparedValueList = config.isPrepared() ? config.getWithAsExprePreparedValueList() : null;\r\n//\r\n//\t\t// 不同数据库, 预编译mysql使用with-as\r\n//\t\tif (valueList != null && withAsExprePreparedValueList != null && withAsExprePreparedValueList.size() > 0) {\r\n//\t\t\twithAsExprePreparedValueList.addAll(valueList);\r\n//\t\t\tvalueList = withAsExprePreparedValueList;\r\n//\t\t\t// 多条POST/PUT/DELETE语句的情况,需要重新初始化\r\n//\t\t\tconfig.setWithAsExprePreparedValueList(new ArrayList<>());\r\n//\t\t}\r\n\r\n\t\tif (valueList != null && valueList.isEmpty() == false) {\r\n\t\t\tfor (int i = 0; i < valueList.size(); i++) {\r\n\t\t\t\tstatement = setArgument(config, statement, i, valueList.get(i));\r\n\t\t\t}\r\n\t\t}\r\n\t\t// statement.close();\r\n\r\n\t\treturn statement;\r\n\t}\r\n\r\n\tpublic PreparedStatement setArgument(@NotNull SQLConfig<T, M, L> config, @NotNull PreparedStatement statement, int index, Object value) throws SQLException {\r\n\t\t//JSON.isBooleanOrNumberOrString(v) 解决 PostgreSQL: Can't infer the SQL type to use for an instance of com.alibaba.fastjson.JSONList\r\n\t\tif (apijson.JSON.isBoolOrNumOrStr(value)) {\r\n\t\t\tstatement.setObject(index + 1, value); //PostgreSQL JDBC 不支持隐式类型转换 tinyint = varchar 报错\r\n\t\t}\r\n\t\telse {\r\n\t\t\tstatement.setString(index + 1, value == null ? null : value.toString()); //MySQL setObject 不支持 JSON 类型\r\n\t\t}\r\n\t\treturn statement;\r\n\t}\r\n\r\n\tprotected Map<String, Connection> connectionMap = new HashMap<>();\r\n\tpublic Map<String, Connection> getConnectionMap() {\r\n\t\tif (connectionMap == null) {\r\n\t\t\tconnectionMap = new HashMap<>();\r\n\t\t}\r\n\t\treturn connectionMap;\r\n\t}\r\n\r\n\tprotected Connection connection;\r\n\t@NotNull\r\n\tpublic Connection getConnection(String key) throws Exception {\r\n\t\treturn getConnectionMap().get(key);\r\n\t}\r\n\tpublic Connection putConnection(String key, Connection connection) throws Exception {\r\n\t\treturn getConnectionMap().put(key, connection);\r\n\t}\r\n\r\n\t@NotNull\r\n\t@Override\r\n\tpublic Connection getConnection(@NotNull SQLConfig<T, M, L> config) throws Exception {\r\n\t\tString connectionKey = getConnectionKey(config);\r\n\t\tconnection = getConnection(connectionKey);\r\n\t\tif (connection == null || connection.isClosed()) {\r\n\t\t\tLog.i(TAG, \"select  connection \" + (connection == null ? \" = null\" : (\"isClosed = \" + connection.isClosed()))) ;\r\n\t\t\t// PostgreSQL 不允许 cross-database\r\n\t\t\tconnection = DriverManager.getConnection(config.gainDBUri(), config.gainDBAccount(), config.gainDBPassword());\r\n\t\t\tputConnection(connectionKey, connection);\r\n\t\t}\r\n\r\n\t\t// TDengine 驱动内部事务处理方法都是空实现，手动 commit 无效\r\n\t\tint ti = config.isTDengine() ? Connection.TRANSACTION_NONE : getTransactionIsolation();\r\n\t\tif (ti != Connection.TRANSACTION_NONE) { //java.sql.SQLException: Transaction isolation level NONE not supported by MySQL\r\n\t\t\tbegin(ti);\r\n\t\t}\r\n\r\n\t\treturn connection;\r\n\t}\r\n\r\n\tpublic String getConnectionKey(@NotNull SQLConfig<T, M, L> config) {\r\n\t\treturn getConnectionKey(config.getDatabase(), config.getDatasource(), config.getNamespace(), config.getCatalog());\r\n\t}\r\n\tpublic String getConnectionKey(String database, String datasource, String namespace, String catalog) {\r\n\t\treturn database + \"-\" + datasource + \"-\" + namespace + \"-\" + catalog;\r\n\t}\r\n\r\n\t//事务处理 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\r\n\tprivate int transactionIsolation;\r\n\t@Override\r\n\tpublic int getTransactionIsolation() {\r\n\t\treturn transactionIsolation;\r\n\t}\r\n\t@Override\r\n\tpublic void setTransactionIsolation(int transactionIsolation) {\r\n\t\tthis.transactionIsolation = transactionIsolation;\r\n\t}\r\n\r\n\tprotected Map<Connection, Integer> isolationMap = new LinkedHashMap<>();\r\n\t@Override\r\n\tpublic void begin(int transactionIsolation) throws SQLException {\r\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<< TRANSACTION begin transactionIsolation = \" + transactionIsolation + \" >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\t\t// 不做判断，如果掩盖了问题，调用层都不知道为啥事务没有提交成功\r\n\t\t//\t\tif (connection == null || connection.isClosed()) {\r\n\t\t//\t\t\treturn;\r\n\t\t//\t\t}\r\n\r\n\t\t// 将所有连接设置隔离级别，且禁止自动提交，需要以下代码来 commit/rollback\r\n\t\tCollection<Connection> connections = connectionMap == null ? null : connectionMap.values();\r\n\t\tif (connections != null) {\r\n\t\t\tfor (Connection connection : connections) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tInteger isolation = isolationMap.get(connection);\r\n\t\t\t\t\tif (isolation == null || isolation != transactionIsolation) { // 只设置一次 Isolation 等级 PG 及 MySQL 某些版本重复设置事务等级会报错\r\n\t\t\t\t\t\tisolationMap.put(connection, transactionIsolation);\r\n\r\n\t\t\t\t\t\tconnection.setTransactionIsolation(transactionIsolation); // 这句导致 TDengine 驱动报错\r\n\t\t\t\t\t\tif (isolation == null) {\r\n\t\t\t\t\t\t\tconnection.setAutoCommit(false); // java.sql.SQLException: Can''t call commit when autocommit=true\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch (SQLException e) {\r\n\t\t\t\t\tLog.e(TAG, \"setAutoCommit failed in rollback\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void rollback() throws SQLException {\r\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<< TRANSACTION rollback >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\t\t//权限校验不通过，connection 也不会生成，还是得判断  //不做判断，如果掩盖了问题，调用层都不知道为啥事务没有提交成功\r\n//\t\tif (connection == null) { // || connection.isClosed()) {\r\n//\t\t\treturn;\r\n//\t\t}\r\n\r\n\t\t// 将所有连接进行回滚\r\n\t\tCollection<Connection> connections = connectionMap == null ? null : connectionMap.values();\r\n\t\tif (connections != null) {\r\n\t\t\tfor (Connection connection : connections) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tif (connection != null && connection.isClosed() == false) {\r\n\t\t\t\t\t\tconnection.rollback();\r\n\t\t\t\t\t\tconnection.setAutoCommit(true);\r\n\r\n\t\t\t\t\t\tisolationMap.remove(connection);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch (SQLException e) {\r\n\t\t\t\t\tLog.e(TAG, \"rollback failed\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void rollback(Savepoint savepoint) throws SQLException {\r\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<< TRANSACTION rollback savepoint \" + (savepoint == null ? \"\" : \"!\") + \"= null >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\t\tif (savepoint == null) {\r\n\t\t\trollback();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t//权限校验不通过，connection 也不会生成，还是得判断  //不做判断，如果掩盖了问题，调用层都不知道为啥事务没有提交成功\r\n//\t\tif (connection == null) { // || connection.isClosed()) {\r\n//\t\t\treturn;\r\n//\t\t}\r\n\r\n\t\t// 将所有连接进行回滚\r\n\t\tCollection<Connection> connections = connectionMap == null ? null : connectionMap.values();\r\n\t\tif (connections != null) {\r\n\t\t\tfor (Connection connection : connections) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tif (connection != null && connection.isClosed() == false) {\r\n\t\t\t\t\t\tconnection.rollback(savepoint);\r\n\t\t\t\t\t\tconnection.setAutoCommit(true);\r\n\r\n\t\t\t\t\t\tisolationMap.remove(connection);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch (SQLException e) {\r\n\t\t\t\t\tLog.e(TAG, \"rollback with savepoint failed\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t@Override\r\n\tpublic void commit() throws SQLException {\r\n\t\tLog.d(\"\\n\\n\" + TAG, \"<<<<<<<<<<<<<< TRANSACTION commit >>>>>>>>>>>>>>>>>>>>>>> \\n\\n\");\r\n\t\t//权限校验不通过，connection 也不会生成，还是得判断  //不做判断，如果掩盖了问题，调用层都不知道为啥事务没有提交成功\r\n//\t\tif (connection == null) { // || connection.isClosed()) {\r\n//\t\t\treturn;\r\n//\t\t}\r\n\t\t\r\n\t\t// 将所有连接进行提交\r\n\t\tCollection<Connection> connections = connectionMap == null ? null : connectionMap.values();\r\n\t\tif (connections != null) {\r\n\t\t\tfor (Connection connection : connections) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tif (connection != null && connection.isClosed() == false) {\r\n\t\t\t\t\t\tconnection.commit();\r\n\r\n\t\t\t\t\t\tisolationMap.remove(connection);\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch (SQLException e) {\r\n\t\t\t\t\tLog.e(TAG, \"commit failed\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\t//事务处理 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\r\n\r\n\t/**关闭连接，释放资源\r\n\t */\r\n\t@Override\r\n\tpublic void close() {\r\n\t\tcacheMap.clear();\r\n\t\tcacheMap = null;\r\n\r\n\t\tgeneratedSQLCount = 0;\r\n\t\tcachedSQLCount = 0;\r\n\t\texecutedSQLCount = 0;\r\n\r\n\t\tif (connectionMap == null || connectionMap.isEmpty()) {\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tCollection<Connection> connections = connectionMap.values();\r\n\t\tif (connections != null) {\r\n\t\t\tfor (Connection connection : connections) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tif (connection != null && connection.isClosed() == false) {\r\n\t\t\t\t\t\tconnection.close();\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tcatch (SQLException e) {\r\n\t\t\t\t\tLog.e(TAG, \"close connection failed\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tconnectionMap.clear();\r\n\t\tconnectionMap = null;\r\n\t}\r\n\r\n\t@Override\r\n\tpublic ResultSet executeQuery(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception {\r\n\t\tif (config.isPrepared() == false || config.isTDengine() // TDengine JDBC 不支持 PreparedStatement\r\n            || (config.isExplain() && (config.isPresto() || config.isTrino()))) { // Presto JDBC 0.277 在 EXPLAIN 模式下预编译值不会替代 ? 占位导致报错\r\n\r\n            Connection conn = getConnection(config);\r\n            Statement stt = conn.createStatement();\r\n            // Statement stt = config.isTDengine()\r\n            //        ? conn.createStatement() // fix Presto: ResultSet: Exception: set type is TYPE_FORWARD_ONLY, Result set concurrency must be CONCUR_READ_ONLY\r\n            //        : conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);\r\n\r\n            return executeQuery(stt, StringUtil.isEmpty(sql) ? config.gainSQL(false) : sql);\r\n\t\t}\r\n\r\n        // Presto JDBC 0.277 在 EXPLAIN 模式下预编译值不会替代 ? 占位导致报错\r\n\t\tPreparedStatement stt = getStatement(config, sql);\r\n\t\tResultSet rs = stt.executeQuery();  //PreparedStatement 不用传 SQL\r\n\t\t//\t\tif (config.isExplain() && (config.isSQLServer() || config.isOracle())) {\r\n\t\t// FIXME 返回的是 boolean 值\t\t\trs = stt.getMoreResults(Statement.CLOSE_CURRENT_RESULT);\r\n\t\t//\t\t}\r\n\r\n\t\treturn rs;\r\n\t}\r\n\r\n\r\n\t@Override\r\n\tpublic int executeUpdate(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception {\r\n\t\tStatement stt;\r\n\t\tint count;\r\n\t\tif (config.isTDengine()) {\r\n\t\t\tConnection conn = getConnection(config);\r\n            stt = conn.createStatement();\r\n\t\t\t//stt = config.isTDengine()\r\n            //        ? conn.createStatement() // fix Presto: ResultSet: Exception: set type is TYPE_FORWARD_ONLY, Result set concurrency must be CONCUR_READ_ONLY\r\n            //        : conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT);\r\n\r\n            count = stt.executeUpdate(StringUtil.isEmpty(sql) ? config.gainSQL(false) : sql);\r\n\t\t}\r\n\t\telse {\r\n\t\t\tstt = getStatement(config);\r\n\t\t\tcount = ((PreparedStatement) stt).executeUpdate();  // PreparedStatement 不用传 SQL\r\n\t\t}\r\n\r\n\t\tif (count <= 0 && config.isHive()) {\r\n\t\t\tcount = 1;\r\n\t\t}\r\n\r\n\t\tif (config.getId() == null && config.getMethod() == RequestMethod.POST) {  // 自增id\r\n\t\t\tResultSet rs = stt.getGeneratedKeys();\r\n\t\t\tif (rs != null && rs.next()) {\r\n\t\t\t\tconfig.setId(rs.getLong(1));\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn count;\r\n\t}\r\n\r\n\r\n}\r\n\r\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport static apijson.JSON.*;\nimport static apijson.JSONMap.*;\nimport static apijson.RequestMethod.DELETE;\nimport static apijson.RequestMethod.GET;\nimport static apijson.RequestMethod.GETS;\nimport static apijson.RequestMethod.HEAD;\nimport static apijson.RequestMethod.HEADS;\nimport static apijson.RequestMethod.POST;\nimport static apijson.RequestMethod.PUT;\nimport static apijson.orm.Operation.*;\n//import static apijson.orm.Operation.CODE;\n\nimport java.net.URL;\nimport java.time.LocalDate;\nimport java.time.LocalDateTime;\nimport java.time.LocalTime;\nimport java.util.*;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\nimport apijson.*;\n\nimport apijson.orm.AbstractSQLConfig.IdCallback;\nimport apijson.orm.exception.ConflictException;\nimport apijson.orm.exception.NotLoggedInException;\nimport apijson.orm.exception.UnsupportedDataTypeException;\nimport apijson.orm.model.Access;\nimport apijson.orm.model.Column;\nimport apijson.orm.model.Document;\nimport apijson.orm.model.ExtendedProperty;\nimport apijson.orm.model.Function;\nimport apijson.orm.model.Script;\nimport apijson.orm.model.PgAttribute;\nimport apijson.orm.model.PgClass;\nimport apijson.orm.model.Request;\nimport apijson.orm.model.SysColumn;\nimport apijson.orm.model.SysTable;\nimport apijson.orm.model.Table;\nimport apijson.orm.model.AllTable;\nimport apijson.orm.model.AllColumn;\nimport apijson.orm.model.AllTableComment;\nimport apijson.orm.model.AllColumnComment;\nimport apijson.orm.model.TestRecord;\n\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\n\n/**校验器(权限、请求参数、返回结果等)\n * TODO 合并 Structure 的代码\n * @author Lemon\n * @param <T> id 与 userId 的类型，一般为 Long\n */\npublic abstract class AbstractVerifier<T, M extends Map<String, Object>, L extends List<Object>>\n\t\timplements Verifier<T, M, L>, IdCallback<T> {\n\tprivate static final String TAG = \"AbstractVerifier\";\n\n\t/**为 PUT, DELETE 强制要求必须有 id/id{}/id{}@ 条件\n\t */\n\tpublic static boolean IS_UPDATE_MUST_HAVE_ID_CONDITION = true;\n\t/**开启校验请求角色权限\n\t*/\n\tpublic static boolean ENABLE_VERIFY_ROLE = true;\n\t/**开启校验请求传参内容\n\t*/\n\tpublic static boolean ENABLE_VERIFY_CONTENT = true;\n\n\t/**未登录，不明身份的用户\n\t */\n\tpublic static final String UNKNOWN = \"UNKNOWN\";\n\n\t/**已登录的用户\n\t */\n\tpublic static final String LOGIN = \"LOGIN\";\n\n\t/**联系人，必须已登录\n\t */\n\tpublic static final String CONTACT = \"CONTACT\";\n\n\t/**圈子成员(CONTACT + OWNER)，必须已登录\n\t */\n\tpublic static final String CIRCLE = \"CIRCLE\";\n\n\t/**拥有者，必须已登录\n\t */\n\tpublic static final String OWNER = \"OWNER\";\n\n\t/**管理员，必须已登录\n\t */\n\tpublic static final String ADMIN = \"ADMIN\";\n\n//\tpublic static ParserCreator<T, M, L> PARSER_CREATOR;\n\n\tpublic static ScriptEngineManager SCRIPT_ENGINE_MANAGER;\n\tpublic static ScriptEngine SCRIPT_ENGINE;\n\n\t// 共享 STRUCTURE_MAP 则不能 remove 等做任何变更，否则在并发情况下可能会出错，加锁效率又低，所以这里改为忽略对应的 key\n\tpublic static Map<String, Entry<String, Object>> ROLE_MAP;\n\n\tpublic static List<String> OPERATION_KEY_LIST;\n\n\t// <TableName, <METHOD, allowRoles>>\n\t// <User, <GET, [OWNER, ADMIN]>>\n\t@NotNull\n\tpublic static Map<String, Map<RequestMethod, String[]>> SYSTEM_ACCESS_MAP;\n\t@NotNull\n\tpublic static Map<String, Map<RequestMethod, String[]>> ACCESS_MAP;\n\t@NotNull\n\tpublic static Map<String, Map<String, Object>> ACCESS_FAKE_DELETE_MAP;\n\n\t// <method tag, <version, Request>>\n\t// <PUT Comment, <1, { \"method\":\"PUT\", \"tag\":\"Comment\", \"structure\":{ \"MUST\":\"id\"... }... }>>\n\t@NotNull\n\tpublic static Map<String, SortedMap<Integer, Map<String, Object>>> REQUEST_MAP;\n\tprivate static String VERIFY_LENGTH_RULE = \"(?<first>[>=<]*)(?<second>[0-9]*)\";\n\tprivate static Pattern VERIFY_LENGTH_PATTERN = Pattern.compile(VERIFY_LENGTH_RULE);\n\n\t// 正则匹配的别名快捷方式，例如用 \"PHONE\" 代替 \"^((13[0-9])|(15[^4,\\\\D])|(18[0-2,5-9])|(17[0-9]))\\\\d{8}$\"\n\t@NotNull\n\tpublic static final Map<String, Pattern> COMPILE_MAP;\n\tstatic {\n\t\tSCRIPT_ENGINE_MANAGER = new ScriptEngineManager();\n\t\tSCRIPT_ENGINE = SCRIPT_ENGINE_MANAGER.getEngineByName(\"js\");\n\n\t\tROLE_MAP = new LinkedHashMap<>();\n\t\tROLE_MAP.put(UNKNOWN, new Entry<String, Object>());\n\t\tROLE_MAP.put(LOGIN, new Entry<String, Object>(\"userId>\", 0));\n\t\tROLE_MAP.put(CONTACT, new Entry<String, Object>(\"userId{}\", \"contactIdList\"));\n\t\tROLE_MAP.put(CIRCLE, new Entry<String, Object>(\"userId-()\", \"verifyCircle()\")); // \"userId{}\", \"circleIdList\"));  // 还是 {\"userId\":\"currentUserId\", \"userId{}\": \"contactIdList\", \"@combine\": \"userId,userId{}\" } ?\n\t\tROLE_MAP.put(OWNER, new Entry<String, Object>(\"userId\", \"userId\"));\n\t\tROLE_MAP.put(ADMIN, new Entry<String, Object>(\"userId-()\", \"verifyAdmin()\"));\n\n\t\tOPERATION_KEY_LIST = new ArrayList<>();\n\t\tOPERATION_KEY_LIST.add(TYPE.name());\n\t\tOPERATION_KEY_LIST.add(VERIFY.name());\n\t\tOPERATION_KEY_LIST.add(INSERT.name());\n\t\tOPERATION_KEY_LIST.add(UPDATE.name());\n\t\tOPERATION_KEY_LIST.add(REPLACE.name());\n\t\tOPERATION_KEY_LIST.add(EXIST.name());\n\t\tOPERATION_KEY_LIST.add(UNIQUE.name());\n\t\tOPERATION_KEY_LIST.add(REMOVE.name());\n\t\tOPERATION_KEY_LIST.add(MUST.name());\n\t\tOPERATION_KEY_LIST.add(REFUSE.name());\n\t\tOPERATION_KEY_LIST.add(IF.name());\n//\t\tOPERATION_KEY_LIST.add(CODE.name());\n\t\tOPERATION_KEY_LIST.add(ALLOW_PARTIAL_UPDATE_FAIL.name());\n\n\n\t\tSYSTEM_ACCESS_MAP = new HashMap<String, Map<RequestMethod, String[]>>();\n\n\t\tSYSTEM_ACCESS_MAP.put(Access.class.getSimpleName(), getAccessMap(Access.class.getAnnotation(MethodAccess.class)));\n\t\tSYSTEM_ACCESS_MAP.put(Function.class.getSimpleName(), getAccessMap(Function.class.getAnnotation(MethodAccess.class)));\n\t\tSYSTEM_ACCESS_MAP.put(Script.class.getSimpleName(), getAccessMap(Script.class.getAnnotation(MethodAccess.class)));\n\t\tSYSTEM_ACCESS_MAP.put(Request.class.getSimpleName(), getAccessMap(Request.class.getAnnotation(MethodAccess.class)));\n\n\t\tif (Log.DEBUG) {\n\t\t\tSYSTEM_ACCESS_MAP.put(Table.class.getSimpleName(), getAccessMap(Table.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(Column.class.getSimpleName(), getAccessMap(Column.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(PgAttribute.class.getSimpleName(), getAccessMap(PgAttribute.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(PgClass.class.getSimpleName(), getAccessMap(PgClass.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(AllTable.class.getSimpleName(), getAccessMap(AllTable.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(AllTableComment.class.getSimpleName(), getAccessMap(AllTableComment.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(AllColumn.class.getSimpleName(), getAccessMap(AllColumn.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(AllColumnComment.class.getSimpleName(), getAccessMap(AllColumnComment.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(SysTable.class.getSimpleName(), getAccessMap(SysTable.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(SysColumn.class.getSimpleName(), getAccessMap(SysColumn.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(ExtendedProperty.class.getSimpleName(), getAccessMap(ExtendedProperty.class.getAnnotation(MethodAccess.class)));\n\n\t\t\tSYSTEM_ACCESS_MAP.put(Document.class.getSimpleName(), getAccessMap(Document.class.getAnnotation(MethodAccess.class)));\n\t\t\tSYSTEM_ACCESS_MAP.put(TestRecord.class.getSimpleName(), getAccessMap(TestRecord.class.getAnnotation(MethodAccess.class)));\n\t\t}\n\n\t\tACCESS_MAP = new HashMap<>(SYSTEM_ACCESS_MAP);\n\n\t\tREQUEST_MAP = new HashMap<>(ACCESS_MAP.size()*7);  // 单个与批量增删改\n\n\t\tCOMPILE_MAP = new HashMap<String, Pattern>();\n\n\t}\n\n\n\t/**获取权限Map，每种操作都只允许对应的角色\n\t * @param access\n\t * @return\n\t */\n\tpublic static HashMap<RequestMethod, String[]> getAccessMap(MethodAccess access) {\n\t\tif (access == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tHashMap<RequestMethod, String[]> map = new HashMap<>();\n\t\tmap.put(GET, access.GET());\n\t\tmap.put(HEAD, access.HEAD());\n\t\tmap.put(GETS, access.GETS());\n\t\tmap.put(HEADS, access.HEADS());\n\t\tmap.put(POST, access.POST());\n\t\tmap.put(PUT, access.PUT());\n\t\tmap.put(DELETE, access.DELETE());\n\n\t\treturn map;\n\t}\n\n\n\t@Override\n\tpublic String getVisitorIdKey(SQLConfig<T, M, L> config) {\n\t\treturn config == null ? getUserIdKey(null, null, null, null) : config.getUserIdKey();\n\t}\n\n\t@Override\n\tpublic String getIdKey(String database, String schema, String datasource, String table) {\n\t\treturn KEY_ID;\n\t}\n\t@Override\n\tpublic String getUserIdKey(String database, String schema, String datasource, String table) {\n\t\treturn KEY_USER_ID;\n\t}\n\n\t@SuppressWarnings(\"unchecked\")\n\t@Override\n\tpublic T newId(RequestMethod method, String database, String schema, String datasource, String table) {\n\t\treturn (T) Long.valueOf(System.currentTimeMillis());\n\t}\n\n\n\n\t@NotNull\n\tprotected Visitor<T> visitor;\n\tprotected Object visitorId;\n\t@NotNull\n\t@Override\n\tpublic Visitor<T> getVisitor() {\n\t\treturn visitor;\n\t}\n\t@Override\n\tpublic AbstractVerifier<T, M, L> setVisitor(Visitor<T> visitor) {\n\t\tthis.visitor = visitor;\n\t\tthis.visitorId = visitor == null ? null : visitor.getId();\n\n\t\t//导致内部调用且放行校验(needVerifyLogin, needVerifyRole)也抛异常\n\t\t//\t\tif (visitorId == null) {\n\t\t//\t\t\tthrow new NullPointerException(TAG + \".setVisitor visitorId == null !!! 可能导致权限校验失效，引发安全问题！\");\n\t\t//\t\t}\n\n\t\treturn this;\n\t}\n\n\n\t/**验证权限是否通过\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic boolean verifyAccess(SQLConfig<T, M, L> config) throws Exception {\n\t\tif (ENABLE_VERIFY_ROLE == false) {\n\t\t\tthrow new UnsupportedOperationException(\"AbstractVerifier.ENABLE_VERIFY_ROLE == false \" +\n                    \"时不支持校验角色权限！如需支持则设置 AbstractVerifier.ENABLE_VERIFY_ROLE = true ！\");\n\t\t}\n\n\t\tString table = config == null ? null : config.getTable();\n\t\tif (table == null) {\n\t\t\treturn true;\n\t\t}\n\n\t\tString role = config.getRole();\n\t\tif (role == null) {\n\t\t\trole = UNKNOWN;\n\t\t}\n\t\telse {\n\t\t\tif (ROLE_MAP.containsKey(role) == false) {\n\t\t\t\tSet<String> NAMES = ROLE_MAP.keySet();\n\t\t\t\tthrow new IllegalArgumentException(\"角色 \" + role + \" 不存在！\" +\n                        \"只能是[\" + StringUtil.get(NAMES.toArray()) + \"]中的一种！\");\n\t\t\t}\n\n\t\t\tif (role.equals(UNKNOWN) == false) { //未登录的角色\n\t\t\t\tverifyLogin();\n\t\t\t}\n\t\t}\n\n\t\tRequestMethod method = config.getMethod();\n\t\tverifyRole(config, table, method, role);\n\n\t\treturn true;\n\t}\n\n\t@Override\n\tpublic void verifyRole(SQLConfig<T, M, L> config, String table, RequestMethod method, String role) throws Exception {\n\t\tverifyAllowRole(config, table, method, role); //验证允许的角色\n\t\tverifyUseRole(config, table, method, role); //验证使用的角色\n\t}\n\n\t/**允许请求使用的所以可能角色\n\t * @param config\n\t * @param table\n\t * @param method\n\t * @param role\n\t * @return\n\t * @throws Exception\n\t * @see {@link JSONMap#KEY_ROLE}\n\t */\n\tpublic void verifyAllowRole(SQLConfig<T, M, L> config, String table, RequestMethod method, String role) throws Exception {\n\t\tLog.d(TAG, \"verifyAllowRole  table = \" + table + \"; method = \" + method + \"; role = \" + role);\n\t\tif (table == null) {\n\t\t\ttable = config == null ? null : config.getTable();\n\t\t}\n\n\t\tif (table != null) {\n\t\t\tif (method == null) {\n\t\t\t\tmethod = config == null ? GET : config.getMethod();\n\t\t\t}\n\t\t\tif (role == null) {\n\t\t\t\trole = config == null ? UNKNOWN : config.getRole();\n\t\t\t}\n\n\t\t\tMap<RequestMethod, String[]> map = ACCESS_MAP.get(table);\n\n\t\t\tif (map == null || Arrays.asList(map.get(method)).contains(role) == false) {\n\t\t\t\tthrow new IllegalAccessException(table + \" 不允许 \" + role + \" 用户的 \" + method.name() + \" 请求！\");\n\t\t\t}\n\t\t}\n\t}\n\n\t/**校验请求使用的角色，角色不好判断，让访问者发过来角色名，OWNER,CONTACT,ADMIN等\n\t * @param config\n\t * @param table\n\t * @param method\n\t * @param role\n\t * @return\n\t * @throws Exception\n\t * @see {@link JSONMap#KEY_ROLE}\n\t */\n\tpublic void verifyUseRole(@NotNull SQLConfig<T, M, L> config, String table, RequestMethod method, String role) throws Exception {\n\t\tLog.d(TAG, \"verifyUseRole  table = \" + table + \"; method = \" + method + \"; role = \" + role);\n\t\tObjects.requireNonNull(config);\n\t\t//验证角色，假定真实强制匹配<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t\tif (table == null) {\n\t\t\ttable = config.getTable();\n\t\t}\n\t\tif (method == null) {\n\t\t\tmethod = config.getMethod();\n\t\t}\n\t\tif (role == null) {\n\t\t\trole = config.getRole();\n\t\t}\n\n\t\tString visitorIdKey = getVisitorIdKey(config);\n\n\t\tObject requestId;\n\t\tswitch (role) {\n\t\tcase LOGIN://verifyRole通过就行\n\t\t\tbreak;\n\t\tcase CONTACT:\n\t\tcase CIRCLE:\n\t\t\t// TODO 做一个缓存contactMap<visitorId, contactArray>，提高[]:{}查询性能， removeAccessInfo时map.remove(visitorId)\n\t\t\t// 不能在 Visitor内null -> [] ! 否则会导致某些查询加上不需要的条件！\n\t\t\tList<Object> list = visitor.getContactIdList() == null\n\t\t\t? new ArrayList<Object>() : new ArrayList<Object>(visitor.getContactIdList());\n\t\t\tif (CIRCLE.equals(role)) {\n\t\t\t\tlist.add(visitorId);\n\t\t\t}\n\n\t\t\t// key!{}:[] 或 其它没有明确id的条件 等 可以和 key{}:[] 组合。类型错误就报错\n\t\t\trequestId = config.getWhere(visitorIdKey, true); // JSON 里数值不能保证是 Long，可能是 Integer\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tCollection<Object> requestIdArray = (Collection<Object>) config.getWhere(visitorIdKey + \"{}\", true); // 不能是 &{}， |{} 不要传，直接 {}\n\t\t\tif (requestId != null) {\n\t\t\t\tif (requestIdArray == null) {\n\t\t\t\t\trequestIdArray = createJSONArray();\n\t\t\t\t}\n\t\t\t\trequestIdArray.add(requestId);\n\t\t\t}\n\n\t\t\tif (requestIdArray == null) { // 可能是 @ 得到 || requestIdArray.isEmpty()) { // 请求未声明 key:id 或 key{}:[...] 条件，自动补全\n\t\t\t\tconfig.putWhere(visitorIdKey+\"{}\", parseArray(list), true); // key{}:[] 有效，SQLConfig<T, M, L> 里 throw NotExistException\n\t\t\t}\n\t\t\telse { // 请求已声明 key:id 或 key{}:[] 条件，直接验证\n\t\t\t\tfor (Object id : requestIdArray) {\n\t\t\t\t\tif (id == null) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (id instanceof Number) { // 不能准确地判断 Long，可能是 Integer\n\t\t\t\t\t\tif (((Number) id).longValue() <= 0 || list.contains(Long.valueOf(\"\" + id)) == false) { // Integer等转为 Long 才能正确判断，强转崩溃\n\t\t\t\t\t\t\tthrow new IllegalAccessException(visitorIdKey + \" = \" + id + \" 的 \" + table\n\t\t\t\t\t\t\t\t\t+ \" 不允许 \" + role + \" 用户的 \" + method.name() + \" 请求！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse if (id instanceof String) {\n\t\t\t\t\t\tif (StringUtil.isEmpty(id) || list.contains(id) == false) {\n\t\t\t\t\t\t\tthrow new IllegalAccessException(visitorIdKey + \" = \" + id + \" 的 \" + table\n\t\t\t\t\t\t\t\t\t+ \" 不允许 \" + role + \" 用户的 \" + method.name() + \" 请求！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {\n\t\t\t\t\t\tthrow new UnsupportedDataTypeException(table + \".id 类型错误，类型必须是 Long/String！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\tcase OWNER:\n\t\t\tif (config.getMethod() == POST) {\n\t\t\t\tList<String> c = config.getColumn();\n\t\t\t\tList<List<Object>> ovs = config.getValues();\n\t\t\t\tif ( (c == null || c.isEmpty()) || (ovs == null || ovs.isEmpty()) ) {\n\t\t\t\t\tthrow new IllegalArgumentException(\"POST 请求必须在Table内设置要保存的 key:value ！\");\n\t\t\t\t}\n\n\t\t\t\tint index = c.indexOf(visitorIdKey);\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\tObject oid;\n\t\t\t\t\tfor (List<Object> ovl : ovs) {\n\t\t\t\t\t\toid = ovl == null || index >= ovl.size() ? null : ovl.get(index);\n\t\t\t\t\t\tif (oid == null || StringUtil.get(oid).equals(\"\" + visitorId) == false) {\n\t\t\t\t\t\t\tthrow new IllegalAccessException(visitorIdKey + \" = \" + oid + \" 的 \" + table\n\t\t\t\t\t\t\t\t\t+ \" 不允许 \" + role + \" 用户的 \" + method.name() + \" 请求！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tList<String> nc = new ArrayList<>(c);\n\t\t\t\t\tnc.add(visitorIdKey);\n\t\t\t\t\tconfig.setColumn(nc);\n\n\t\t\t\t\tList<List<Object>> nvs = new ArrayList<>();\n\t\t\t\t\tList<Object> nvl;\n\t\t\t\t\tfor (List<Object> ovl : ovs) {\n\t\t\t\t\t\tnvl = ovl == null || ovl.isEmpty() ? new ArrayList<>() : new ArrayList<>(ovl);\n\t\t\t\t\t\tnvl.add(visitorId);\n\t\t\t\t\t\tnvs.add(nvl);\n\t\t\t\t\t}\n\n\t\t\t\t\tconfig.setValues(nvs);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\trequestId = config.getWhere(visitorIdKey, true);//JSON里数值不能保证是Long，可能是Integer\n\t\t\t\tif (requestId != null && StringUtil.get(requestId).equals(StringUtil.get(visitorId)) == false) {\n\t\t\t\t\tthrow new IllegalAccessException(visitorIdKey + \" = \" + requestId + \" 的 \" + table\n\t\t\t\t\t\t\t+ \" 不允许 \" + role + \" 用户的 \" + method.name() + \" 请求！\");\n\t\t\t\t}\n\n\t\t\t\tconfig.putWhere(visitorIdKey, visitorId, true);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase ADMIN://这里不好做，在特定接口内部判。 可以是  /get/admin + 固定秘钥  Parser#needVerify，之后全局跳过验证\n\t\t\tverifyAdmin();\n\t\t\tbreak;\n\t\tdefault://unknown，verifyRole通过就行\n\t\t\tbreak;\n\t\t}\n\n\t\t//验证角色，假定真实强制匹配>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\t}\n\n\n\t/**登录校验\n\t */\n\t@Override\n\tpublic void verifyLogin() throws Exception {\n\t\t//未登录没有权限操作\n\t\tif (visitorId == null) {\n\t\t\tthrow new NotLoggedInException(\"未登录或登录过期，请登录后再操作！\");\n\t\t}\n\n\t\tif (visitorId instanceof Number) {\n\t\t\tif (((Number) visitorId).longValue() <= 0) {\n\t\t\t\tthrow new NotLoggedInException(\"未登录或登录过期，请登录后再操作！\");\n\t\t\t}\n\t\t}\n\t\telse if (visitorId instanceof String) {\n\t\t\tif (StringUtil.isEmpty(visitorId, true)) {\n\t\t\t\tthrow new NotLoggedInException(\"未登录或登录过期，请登录后再操作！\");\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tthrow new UnsupportedDataTypeException(\"visitorId 只能是 Long 或 String 类型！\");\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic void verifyAdmin() throws Exception {\n\t\tthrow new UnsupportedOperationException(\"不支持 ADMIN 角色！如果要支持就在子类重写这个方法\" +\n                \"来校验 ADMIN 角色，不通过则 throw IllegalAccessException!\");\n\t}\n\n\n\t/**验证是否重复\n\t * FIXME 这个方法实际上没有被使用\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic void verifyRepeat(String table, String key, Object value) throws Exception {\n\t\tverifyRepeat(table, key, value, 0);\n\t}\n\t/**验证是否重复\n\t * FIXME 这个方法实际上没有被使用，而且与 Structure.verifyRepeat 代码重复度比较高，需要简化\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @param exceptId 不包含id\n\t * @throws Exception\n\t */\n\t@Override\n\tpublic void verifyRepeat(String table, String key, Object value, long exceptId) throws Exception {\n\t\tif (key == null || value == null) {\n\t\t\tLog.e(TAG, \"verifyRepeat  key == null || value == null >> return;\");\n\t\t\treturn;\n\t\t}\n\t\tif (value instanceof JSON) {\n\t\t\tthrow new UnsupportedDataTypeException(key + \":value 中value的类型不能为JSON！\");\n\t\t}\n\n\t\tM tblObj = createJSONObject();\n\t\ttblObj.put(key, value);\n\t\tif (exceptId > 0) {//允许修改自己的属性为该属性原来的值\n\t\t\ttblObj.put(KEY_ID + \"!\", exceptId);  // FIXME 这里 id 写死了，不支持自定义\n\t\t}\n\n\t\tM req = createJSONObject();\n\t\treq.put(table, tblObj);\n\t\tMap<String, Object> repeat = createParser().setMethod(HEAD).setNeedVerify(true).parseResponse(req);\n\n\t\trepeat = repeat == null ? null : get(repeat, table);\n\t\tif (repeat == null) {\n\t\t\tthrow new Exception(\"服务器内部错误  verifyRepeat  repeat == null\");\n\t\t}\n\t\tif (getIntValue(repeat, JSONResponse.KEY_COUNT) > 0) {\n\t\t\tthrow new ConflictException(key + \": \" + value + \" 已经存在，不能重复！\");\n\t\t}\n\t}\n\n\n\n\t/**从request提取target指定的内容\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param request\n\t* @param maxUpdateCount\n\t* @param database\n\t* @param schema\n\t* @return\n\t* @throws Exception\n\t*/\n\t@Override\n\tpublic M verifyRequest(@NotNull final RequestMethod method, final String name, final M target, final M request, final int maxUpdateCount\n\t\t\t, final String database, final String schema) throws Exception {\n\t\treturn verifyRequest(method, name, target, request, maxUpdateCount, database, schema, this, getParser());\n\t}\n\n\t/**从request提取target指定的内容\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param request\n\t * @param parser\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M verifyRequest(\n\t\t\t@NotNull final RequestMethod method, final String name, final M target, final M request\n\t\t\t, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\treturn verifyRequest(method, name, target, request, AbstractParser.MAX_UPDATE_COUNT, parser);\n\t}\n\t/**从request提取target指定的内容\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param request\n\t * @param maxUpdateCount\n\t * @param parser\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M verifyRequest(\n\t\t\t@NotNull final RequestMethod method, final String name, final M target, final M request\n            , final int maxUpdateCount, @NotNull Parser<T, M, L> parser) throws Exception {\n\n\t\treturn verifyRequest(method, name, target, request, maxUpdateCount, null, null, null, parser);\n\t}\n\n\t/**从request提取target指定的内容\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param request\n\t* @param maxUpdateCount\n\t* @param database\n\t* @param schema\n\t* @param idCallback\n\t* @param parser\n\t* @return\n\t* @param <T>\n\t* @throws Exception\n\t*/\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M verifyRequest(\n\t\t\t@NotNull RequestMethod method, String name, M target, M request, int maxUpdateCount, String database\n\t\t\t, String schema, IdCallback<T> idCallback, @NotNull Parser<T, M, L> parser) throws Exception {\n\n\t\treturn verifyRequest(method, name, target, request, maxUpdateCount, database, schema, null, idCallback, parser);\n\t}\n\t/**从request提取target指定的内容\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param request\n\t* @param maxUpdateCount\n\t* @param database\n\t* @param schema\n\t* @param datasource\n\t* @param idCallback\n\t* @param parser\n\t* @return\n\t* @param <T>\n\t* @throws Exception\n\t*/\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M verifyRequest(\n\t\t\t@NotNull final RequestMethod method, final String name, final M target, final M request\n            , final int maxUpdateCount, final String database, final String schema, final String datasource\n            , final IdCallback<T> idCallback, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (ENABLE_VERIFY_CONTENT == false) {\n\t\t\tthrow new UnsupportedOperationException(\"AbstractVerifier.ENABLE_VERIFY_CONTENT == false\" +\n                    \" 时不支持校验请求传参内容！如需支持则设置 AbstractVerifier.ENABLE_VERIFY_CONTENT = true ！\");\n\t\t}\n\n\t\tLog.i(TAG, \"verifyRequest  method = \" + method  + \"; name = \" + name\n\t\t\t\t+ \"; target = \\n\" + toJSONString(target)\n\t\t\t\t+ \"\\n request = \\n\" + toJSONString(request));\n\n\t\tif (target == null || request == null) {// || request.isEmpty()) {\n\t\t\tLog.i(TAG, \"verifyRequest  target == null || request == null >> return null;\");\n\t\t\treturn null;\n\t\t}\n\n\t\t//已在 Verifier 中处理\n\t\t//\t\tif (get(getString(request, apijson.JSONMap.KEY_ROLE)) == ADMIN) {\n\t\t//\t\t\tthrow new IllegalArgumentException(\"角色设置错误！不允许在写操作Request中传 \" + name +\n\t\t//\t\t\t\t\t\":{ \" + apijson.JSONMap.KEY_ROLE + \":admin } ！\");\n\t\t//\t\t}\n\n\n\t\t//解析\n\t\treturn parse(method, name, target, request, database, schema, idCallback, parser, new OnParseCallback<T, M, L>() {\n\n\t\t\t@Override\n\t\t\tpublic M onParseJSONObject(String key, M tobj, M robj) throws Exception {\n\t\t\t\t//\t\t\t\tLog.i(TAG, \"verifyRequest.parse.onParseJSONObject  key = \" + key + \"; robj = \" + robj);\n\n\t\t\t\tif (robj == null) {\n\t\t\t\t\tif (tobj != null) {//不允许不传Target中指定的Table\n\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，请在 \" + name + \" 内传 \" + key + \":{} ！\");\n\t\t\t\t\t}\n\t\t\t\t} else if (isTableKey(key)) {\n\t\t\t\t\tString db = getString(request, KEY_DATABASE);\n\t\t\t\t\tString sh = getString(request, KEY_SCHEMA);\n\t\t\t\t\tString ds = getString(request, KEY_DATASOURCE);\n\t\t\t\t\tif (StringUtil.isEmpty(db, false)) {\n\t\t\t\t\t\tdb = database;\n\t\t\t\t\t}\n\t\t\t\t\tif (StringUtil.isEmpty(sh, false)) {\n\t\t\t\t\t\tsh = schema;\n\t\t\t\t\t}\n\t\t\t\t\tif (StringUtil.isEmpty(ds, false)) {\n\t\t\t\t\t\tds = datasource;\n\t\t\t\t\t}\n\n\t\t\t\t\tString idKey = idCallback == null ? null : idCallback.getIdKey(db, sh, ds, key);\n\t\t\t\t\tString finalIdKey = StringUtil.isEmpty(idKey, false) ? KEY_ID : idKey;\n\n\t\t\t\t\tif (method == POST) {\n\t\t\t\t\t\tif (robj.containsKey(finalIdKey)) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key + \" 不能传 \" + finalIdKey + \" ！\");\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tBoolean atLeastOne = tobj == null ? null : getBoolean(tobj, Operation.IS_ID_CONDITION_MUST.name());\n\t\t\t\t\t\tif (Boolean.TRUE.equals(atLeastOne) || RequestMethod.isUpdateMethod(method)) {\n\t\t\t\t\t\t\tverifyId(method.name(), name, key, robj, finalIdKey, maxUpdateCount, atLeastOne != null ? atLeastOne : IS_UPDATE_MUST_HAVE_ID_CONDITION);\n\n\t\t\t\t\t\t\tString userIdKey = idCallback == null ? null : idCallback.getUserIdKey(db, sh, ds, key);\n\t\t\t\t\t\t\tString finalUserIdKey = StringUtil.isEmpty(userIdKey, false) ? KEY_USER_ID : userIdKey;\n\t\t\t\t\t\t\tverifyId(method.name(), name, key, robj, finalUserIdKey, maxUpdateCount, false);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn verifyRequest(method, key, tobj, robj, maxUpdateCount, database, schema, idCallback, parser);\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tprotected L onParseJSONArray(String key, L tarray, L rarray) throws Exception {\n\t\t\t\tif ((method == POST || method == PUT) && isArrayKey(key)) {\n\t\t\t\t\tif (rarray == null || rarray.isEmpty()) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，请在 \" + name + \" 内传 \" + key + \":[{ ... }] \"\n\t\t\t\t\t\t\t\t+ \"，批量新增 Table[]:value 中 value 必须是包含表对象的非空数组！其中每个子项 { ... } 都是\"\n\t\t\t\t\t\t\t\t+ \" tag:\" + key.substring(0, key.length() - 2) + \" 对应单个新增的 structure ！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (rarray.size() > maxUpdateCount) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t\t\t+ \" 里面的 \" + key + \":[{ ... }] 中 [] 的长度不能超过 \" + maxUpdateCount + \" ！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn super.onParseJSONArray(key, tarray, rarray);\n\t\t\t}\n\t\t});\n\n\t}\n\n\t/**\n\t * @param method\n\t * @param name\n\t * @param key\n\t * @param robj\n\t * @param idKey\n\t * @param atLeastOne 至少有一个不为null\n\t */\n\tprivate static <T, M extends Map<String, Object>, L extends List<Object>> void verifyId(\n\t\t\t@NotNull String method, @NotNull String name, @NotNull String key\n\t\t\t, @NotNull M robj, @NotNull String idKey, final int maxUpdateCount, boolean atLeastOne) throws Exception {\n\t\t//单个修改或删除\n\t\tObject id = robj.get(idKey); //如果必须传 id ，可在Request表中配置NECESSARY\n\t\tif (id != null && id instanceof Number == false && id instanceof String == false) {\n\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t+ \" 里面的 \" + idKey + \":value 中value的类型只能是 Long 或 String ！\");\n\t\t}\n\n\n\t\t//批量修改或删除\n\t\tString idInKey = idKey + \"{}\";\n\t\t// id引用, 格式: \"id{}@\": \"sql\"\n\t\tString idRefInKey = getString(robj, idKey + \"{}@\");\n\t\tL idIn = null;\n\t\ttry {\n\t\t\tidIn = get(robj, idInKey); //如果必须传 id{} ，可在Request表中配置NECESSARY\n\t\t} catch (Exception e) {\n\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t+ \" 里面的 \" + idInKey + \":value 中value的类型只能是 [Long] ！\");\n\t\t}\n\t\tif (idIn == null) {\n\t\t\tif (atLeastOne && id == null && idRefInKey == null) {\n\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t+ \" 里面 \" + idKey + \",\" + idInKey  + \",\" + (idKey + \"{}@\") + \" 至少传其中一个！\");\n\t\t\t}\n\t\t} else {\n\t\t\tif (idIn.size() > maxUpdateCount) { //不允许一次操作 maxUpdateCount 条以上记录\n\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t+ \" 里面的 \" + idInKey + \":[] 中[]的长度不能超过 \" + maxUpdateCount + \" ！\");\n\t\t\t}\n\t\t\t//解决 id{}: [\"1' OR 1='1'))--\"] 绕过id{}限制\n\t\t\t//new ArrayList<Long>(idIn) 不能检查类型，Java泛型擦除问题，居然能把 [\"a\"] 赋值进去还不报错\n\t\t\tfor (int i = 0; i < idIn.size(); i++) {\n\t\t\t\tObject o = idIn.get(i);\n\t\t\t\tif (o == null) {\n\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t\t+ \" 里面的 \" + idInKey + \":[] 中所有项都不能为 [ null, <= 0 的数字, 空字符串 \\\"\\\" ] 中任何一个 ！\");\n\t\t\t\t}\n\t\t\t\tif (o instanceof Number) {\n\t\t\t\t\t//解决 Windows mysql-5.6.26-winx64 等低于 5.7 的 MySQL 可能 id{}: [0] 生成 id IN(0) 触发 MySQL bug 导致忽略 IN 条件\n\t\t\t\t\t//例如 UPDATE `apijson`.`TestRecord` SET `testAccountId` = -1 WHERE ( (`id` IN (0)) AND (`userId`= 82001) )\n\t\t\t\t\tif (((Number) o).longValue() <= 0) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t\t\t+ \" 里面的 \" + idInKey + \":[] 中所有项都不能为 [ null, <= 0 的数字, 空字符串 \\\"\\\" ] 中任何一个 ！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse if (o instanceof String) {\n\t\t\t\t\tif (StringUtil.isEmpty(o, true)) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t\t\t+ \" 里面的 \" + idInKey + \":[] 中所有项都不能为 [ null, <= 0 的数字, 空字符串 \\\"\\\" ] 中任何一个 ！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name + \"/\" + key\n\t\t\t\t\t\t\t+ \" 里面的 \" + idInKey + \":[] 中所有项的类型都只能是 Long 或 String ！\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\n\t/**校验并将response转换为指定的内容和结构\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param response\n\t* @param database\n\t* @param schema\n\t* @param parser\n\t* @param callback\n\t* @return\n\t* @throws Exception\n\t*/\n\t@Override\n\tpublic M verifyResponse(@NotNull final RequestMethod method, final String name, final M target, final M response\n\t\t\t, final String database, final String schema, @NotNull Parser<T, M, L> parser, OnParseCallback<T, M, L> callback) throws Exception {\n\t\treturn verifyResponse(method, name, target, response, database, schema, this, parser, callback);\n\t}\n\n\t/**校验并将response转换为指定的内容和结构\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param response\n\t* @param parser\n\t* @param callback\n\t* @return\n\t* @throws Exception\n\t*/\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M verifyResponse(@NotNull final RequestMethod method, final String name\n\t\t\t, final M target, final M response, @NotNull Parser<T, M, L> parser, OnParseCallback<T, M, L> callback) throws Exception {\n\t\treturn verifyResponse(method, name, target, response, null, null, null, parser, callback);\n\t}\n\t/**校验并将response转换为指定的内容和结构\n\t* @param method\n\t* @param name\n\t* @param target\n\t* @param response\n\t* @param database\n\t* @param schema\n\t* @param idKeyCallback\n\t* @param parser\n\t* @param callback\n\t* @return\n\t* @param <T>\n\t* @throws Exception\n\t*/\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>>  M verifyResponse(@NotNull final RequestMethod method\n\t\t\t, final String name, final M target, final M response, final String database, final String schema\n\t\t\t, final IdCallback<T> idKeyCallback, @NotNull Parser<T, M, L> parser, OnParseCallback<T, M, L> callback) throws Exception {\n\n\t\tLog.i(TAG, \"verifyResponse  method = \" + method  + \"; name = \" + name\n\t\t\t\t+ \"; target = \\n\" + toJSONString(target)\n\t\t\t\t+ \"\\n response = \\n\" + toJSONString(response));\n\n\t\tif (target == null || response == null) {// || target.isEmpty() {\n\t\t\tLog.i(TAG, \"verifyResponse  target == null || response == null >> return response;\");\n\t\t\treturn response;\n\t\t}\n\n\t\t//解析\n\t\treturn parse(method, name, target, response, database, schema\n                , idKeyCallback, parser, callback != null ? callback : new OnParseCallback<T, M, L>() {\n\t\t\t@Override\n\t\t\tprotected M onParseJSONObject(String key, M tobj, M robj) throws Exception {\n\t\t\t\treturn verifyResponse(method, key, tobj, robj, database, schema, idKeyCallback, parser, callback);\n\t\t\t}\n\t\t});\n\t}\n\n\n\t/**对request和response不同的解析用callback返回\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param real\n\t * @param parser\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M parse(@NotNull final RequestMethod method\n\t\t\t, String name, M target, M real, @NotNull Parser<T, M, L> parser, @NotNull OnParseCallback<T, M, L> callback) throws Exception {\n\t\treturn parse(method, name, target, real, null, null, null, parser, callback);\n\t}\n\t/**对request和response不同的解析用callback返回\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param real\n\t * @param database\n\t * @param schema\n\t * @param idCallback\n\t * @param parser\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M parse(\n\t\t\t@NotNull final RequestMethod method, String name, M target, M real, final String database, final String schema\n            , final IdCallback<T> idCallback, @NotNull Parser<T, M, L> parser, @NotNull OnParseCallback<T, M, L> callback) throws Exception {\n\t\treturn parse(method, name, target, real, database, schema, null, idCallback, parser, callback);\n\t}\n\t/**对request和response不同的解析用callback返回\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param real\n\t * @param database\n\t * @param schema\n\t * @param datasource\n\t * @param idCallback\n\t * @param parser\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M parse(@NotNull final RequestMethod method\n\t\t\t, String name, M target, M real, final String database, final String schema, final String datasource\n            , final IdCallback<T> idCallback, @NotNull Parser<T, M, L> parser, @NotNull OnParseCallback<T, M, L> callback) throws Exception {\n\t\tif (target == null) {\n\t\t\treturn null;\n\t\t}\n\n\t\tObject _if = target.get(IF.name());\n\t\tboolean ifIsStr = _if instanceof String && StringUtil.isNotEmpty(_if, true);\n\t\tM ifObj = ifIsStr == false && _if instanceof Map<?,?> ? (M) _if : null;\n//\t\t\t\t: (_if instanceof String ? new apijson.JSONMap((String) _if, \"\" /* \"throw new Error('')\" */ ) : null);\n\t\tif (ifObj == null && _if != null && ifIsStr == false) {\n//\t\t\tif (_if instanceof List<?>) {\n//\t\t\t}\n\t\t\tthrow new IllegalArgumentException(name + \": { \" + IF.name() + \": value } 中 value 类型错误！只允许 String, JSONRequest！\");\n\t\t}\n\n\t\t// 获取配置<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tM type = get(target, TYPE.name());\n\t\tM verify = get(target, VERIFY.name());\n\t\tM insert = get(target, INSERT.name());\n\t\tM update = get(target, UPDATE.name());\n\t\tM replace = get(target, REPLACE.name());\n\n\t\tString exist = StringUtil.get(getString(target, EXIST.name()));\n\t\tString unique = StringUtil.get(getString(target, UNIQUE.name()));\n\t\tString remove = StringUtil.get(getString(target, REMOVE.name()));\n\t\tString must = StringUtil.get(getString(target, MUST.name()));\n\t\tString refuse = StringUtil.get(getString(target, REFUSE.name()));\n\n//\t\tObject code = target.get(CODE.name());\n\n\t\t// 移除字段<<<<<<<<<<<<<<<<<<<\n\t\tString[] removes = StringUtil.split(remove);\n\t\tif (removes != null && removes.length > 0) {\n\t\t\tfor (String r : removes) {\n\t\t\t\treal.remove(r);\n\t\t\t}\n\t\t}\n\t\t// 移除字段>>>>>>>>>>>>>>>>>>>\n\n\t\t// 判断必要字段是否都有<<<<<<<<<<<<<<<<<<<\n\t\tString[] musts = StringUtil.split(must);\n\t\tSet<String> mustSet = new HashSet<String>();\n\n\t\tif (musts != null && musts.length > 0) {\n\t\t\tfor (String s : musts) {\n\t\t\t\tif (real.get(s) == null && real.get(s+\"@\") == null) {  // 可能传null进来，这里还会通过 real.containsKey(s) == false) {\n\t\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\"\n                            + name + \" 里面不能缺少 \" + s + \" 等[\" + must + \"]内的任何字段！\");\n\t\t\t\t}\n\n\t\t\t\tmustSet.add(s);\n\t\t\t}\n\t\t}\n\t\t// 判断必要字段是否都有>>>>>>>>>>>>>>>>>>>\n\n\t\tString[] sks = StringUtil.split(getString(real, KEY_STRING));\n\t\tString[] trims = StringUtil.split(getString(real, KEY_TRIM));\n\t\tList<String> stringKeyList = sks == null || sks.length <= 0 ? null : Arrays.asList(sks);\n\t\tList<String> trimKeyList = trims == null || trims.length <= 0 ? null : Arrays.asList(trims);\n\n\t\tSet<String> objKeySet = new HashSet<String>(); // 不能用tableKeySet，仅判断 Table:{} 会导致 key:{ Table:{} } 绕过判断\n\n\t\t// 解析内容<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n\n\t\tSet<Map.Entry<String, Object>> set = new LinkedHashSet<>(target.entrySet());\n\t\tif (set.isEmpty() == false) {\n\n\t\t\tfor (Map.Entry<String, Object> entry : set) {\n                String key = entry == null ? null : entry.getKey();\n\t\t\t\tif (key == null || OPERATION_KEY_LIST.contains(key)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n                Object tvalue = entry.getValue();\n                Object rvalue = real.get(key);\n\t\t\t\tif (rvalue != null && stringKeyList != null && stringKeyList.contains(key)) {\n\t\t\t\t\trvalue = toJSONString(rvalue);\n\t\t\t\t}\n\t\t\t\tif (rvalue != null && trimKeyList != null && trimKeyList.contains(key)) {\n\t\t\t\t\trvalue = StringUtil.trim(rvalue);\n\t\t\t\t}\n\n\t\t\t\tif (callback.onParse(key, tvalue, rvalue) == false) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (tvalue instanceof Map<?, ?>) { // JSONRequest，往下一级提取\n\t\t\t\t\tif (rvalue != null && rvalue instanceof Map<?, ?> == false) {\n\t\t\t\t\t\tthrow new UnsupportedDataTypeException(key + \":value 的 value 不合法！类型必须是 OBJECT ，结构为 {} !\");\n\t\t\t\t\t}\n\t\t\t\t\ttvalue = callback.onParseJSONObject(key, (M) tvalue, (M) rvalue);\n\n\t\t\t\t\tobjKeySet.add(key);\n\t\t\t\t} else if (tvalue instanceof List<?>) { // L\n\t\t\t\t\tif (rvalue != null && rvalue instanceof List<?> == false) {\n\t\t\t\t\t\tthrow new UnsupportedDataTypeException(key + \":value 的 value 不合法！类型必须是 ARRAY ，结构为 [] !\");\n\t\t\t\t\t}\n\t\t\t\t\ttvalue = callback.onParseJSONArray(key, (L) tvalue, (L) rvalue);\n\n\t\t\t\t\tif ((method == POST || method == PUT) && isArrayKey(key)) {\n\t\t\t\t\t\tobjKeySet.add(key);\n\t\t\t\t\t}\n\t\t\t\t} else { // 其它Object\n\t\t\t\t\ttvalue = callback.onParseObject(key, tvalue, rvalue);\n\t\t\t\t}\n\n\t\t\t\tif (tvalue != null) { // 可以在target中加上一些不需要客户端传的键值对\n\t\t\t\t\treal.put(key, tvalue);\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\n\t\t// 解析内容>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t\tSet<String> rkset = real.keySet(); // 解析内容并没有改变rkset\n\n\t\t// 解析不允许的字段<<<<<<<<<<<<<<<<<<<\n\t\tString[] refuses = StringUtil.split(refuse);\n\t\tSet<String> refuseSet = new HashSet<String>();\n\n\t\tif (refuses != null && refuses.length > 0) {\n\t\t\tSet<String> notRefuseSet = new HashSet<String>();\n\n\t\t\tfor (String rfs : refuses) {\n\t\t\t\tif (rfs == null) {  // StringUtil.isEmpty(rfs, true) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (rfs.startsWith(\"!\")) {\n\t\t\t\t\trfs = rfs.substring(1);\n\n\t\t\t\t\tif (notRefuseSet.contains(rfs)) {\n\t\t\t\t\t\tthrow new ConflictException(REFUSE.name() + \":value 中出现了重复的 !\"\n                                + rfs + \" ！不允许重复，也不允许一个 key 和取反 !key 同时使用！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (refuseSet.contains(rfs)) {\n\t\t\t\t\t\tthrow new ConflictException(REFUSE.name() + \":value 中同时出现了 \"\n                                + rfs + \" 和 !\" + rfs + \" ！不允许重复，也不允许一个 key 和取反 !key 同时使用！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (rfs.equals(\"\")) { // 所有非 MUST\n                        // 对@key放行，@role,@column,自定义@position等， @key:{ \"Table\":{} } 不会解析内部\n\t\t\t\t\t\tfor (String key : rkset) {\n\t\t\t\t\t\t\tif (key == null || key.startsWith(\"@\") || notRefuseSet.contains(key)\n                                    || mustSet.contains(key) || objKeySet.contains(key)) {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// 支持id ref: id{}@\n\t\t\t\t\t\t\tif (key.endsWith(\"@\") && mustSet.contains(key.substring(0, key.length() - 1))) {\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\trefuseSet.add(key);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse {  // 排除 !key 后再禁传其它的\n\t\t\t\t\t\tnotRefuseSet.add(rfs);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tif (refuseSet.contains(rfs)) {\n\t\t\t\t\t\tthrow new ConflictException(REFUSE.name() + \":value 中出现了重复的 \" + rfs + \" ！\" +\n                                \"不允许重复，也不允许一个 key 和取反 !key 同时使用！\");\n\t\t\t\t\t}\n\t\t\t\t\tif (notRefuseSet.contains(rfs)) {\n\t\t\t\t\t\tthrow new ConflictException(REFUSE.name() + \":value 中同时出现了 \" + rfs + \" 和 !\" + rfs + \" ！\" +\n                                \"不允许重复，也不允许一个 key 和取反 !key 同时使用！\");\n\t\t\t\t\t}\n\n\t\t\t\t\trefuseSet.add(rfs);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 解析不允许的字段>>>>>>>>>>>>>>>>>>>\n\n\t\tSet<String> onKeys = new LinkedHashSet<>();\n\n\t\t// 判断不允许传的key<<<<<<<<<<<<<<<<<<<<<<<<<\n\t\tfor (String rk : rkset) {\n\t\t\tif (rk == null || KEY_STRING.equals(rk) || KEY_TRIM.equals(rk)) {\n\t\t\t\t// ConcurrentModificationException  real.remove(rk);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (refuseSet.contains(rk)) { // 不允许的字段\n\t\t\t\tthrow new IllegalArgumentException(method + \"请求，\" + name\n\t\t\t\t\t\t+ \" 里面不允许传 \" + rk + \" 等\" + StringUtil.get(refuseSet) + \"内的任何字段！\");\n\t\t\t}\n\n\t\t\tif (KEY_COMBINE.equals(rk)) {\n\t\t\t\tthrow new UnsupportedOperationException(method + \" 请求，\" + rk + \" 不合法！\" +\n\t\t\t\t\t\t\"非开放请求不允许传 \" + KEY_COMBINE + \":value ！\");\n\t\t\t}\n\t\t\tif (KEY_KEY.equals(rk)) {\n\t\t\t\tthrow new UnsupportedOperationException(method + \" 请求，\" + rk + \" 不合法！\" +\n\t\t\t\t\t\t\"非开放请求不允许传 \" + KEY_KEY + \":value ！\");\n\t\t\t}\n\n\t\t\tObject rv = real.get(rk);\n\t\t\tif (rv != null && stringKeyList != null && stringKeyList.contains(rk)) {\n\t\t\t\trv = toJSONString(rv);\n\t\t\t}\n\t\t\tif (rv != null && trimKeyList != null && trimKeyList.contains(rk)) {\n\t\t\t\trv = StringUtil.trim(rv);\n\t\t\t}\n\n\t\t\t// 不允许传远程函数，只能后端配置\n\t\t\tif (rk.endsWith(\"()\") && rv instanceof String) {\n\t\t\t\tthrow new UnsupportedOperationException(method + \" 请求，\" + rk + \" 不合法！\" +\n                        \"非开放请求不允许传远程函数 key():\\\"fun()\\\" ！\");\n\t\t\t}\n\n\t\t\t// 不在target内的 key:{}\n\t\t\tif (rk.startsWith(\"@\") == false && rk.endsWith(\"@\") == false && objKeySet.contains(rk) == false) {\n\t\t\t\tif (rv instanceof Map<?, ?>) {\n\t\t\t\t\tthrow new UnsupportedOperationException(method + \" 请求，\"\n                            + name + \" 里面不允许传 \" + rk + \":{} ！\");\n\t\t\t\t}\n\t\t\t\tif ((method == POST || method == PUT)\n                        && rv instanceof List<?> && isArrayKey(rk)) {\n\t\t\t\t\tthrow new UnsupportedOperationException(method + \" 请求，\" + name + \" 里面不允许 \"\n                            + rk + \":[] 等未定义的 Table[]:[{}] 批量操作键值对！\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 先让其它操作符完成\n//\t\t\tif (rv != null) { // || nulls.contains(rk)) {\n//\t\t\t\tonKeys.add(rk);\n//\t\t\t}\n\t\t}\n\t\t// 判断不允许传的key>>>>>>>>>>>>>>>>>>>>>>>>>\n\n\n\n\t\t// 校验与修改Request<<<<<<<<<<<<<<<<<\n\t\t// 在tableKeySet校验后操作，避免 导致put/add进去的Table 被当成原Request的内容\n\t\treal = operate(TYPE, type, real, parser);\n\t\treal = operate(VERIFY, verify, real, parser);\n\t\treal = operate(INSERT, insert, real, parser);\n\t\treal = operate(UPDATE, update, real, parser);\n\t\treal = operate(REPLACE, replace, real, parser);\n\t\t// 校验与修改Request>>>>>>>>>>>>>>>>>\n\n\n\t\tString db = getString(real, KEY_DATABASE);\n\t\tString sh = getString(real, KEY_SCHEMA);\n\t\tString ds = getString(real, KEY_DATASOURCE);\n\t\tif (StringUtil.isEmpty(db, false)) {\n\t\t\tdb = database;\n\t\t}\n\t\tif (StringUtil.isEmpty(sh, false)) {\n\t\t\tsh = schema;\n\t\t}\n\t\tif (StringUtil.isEmpty(ds, false)) {\n\t\t\tds = datasource;\n\t\t}\n\t\tString idKey = idCallback == null ? null : idCallback.getIdKey(db, sh, ds, name);\n\t\tString finalIdKey = StringUtil.isEmpty(idKey, false) ? KEY_ID : idKey;\n\n\t\t// TODO 放在operate前？考虑性能、operate修改后再验证的值是否和原来一样\n\t\t// 校验存在<<<<<<<<<<<<<<<<<<<\n\t\tString[] exists = StringUtil.split(exist);\n\t\tif (exists != null && exists.length > 0) {\n\t\t\tlong exceptId = getLongValue(real, finalIdKey);\n\t\t\tMap<String,Object> map = new HashMap<>();\n\t\t\tfor (String e : exists) {\n\t\t\t\tmap.put(e,real.get(e));\n\t\t\t}\n\t\t\tverifyExist(name, map, exceptId, parser);\n\t\t}\n\t\t// 校验存在>>>>>>>>>>>>>>>>>>>\n\n\t\t// TODO 放在operate前？考虑性能、operate修改后再验证的值是否和原来一样\n\t\t// 校验重复<<<<<<<<<<<<<<<<<<<\n\t\tString[] uniques = StringUtil.split(unique);\n\t\tif (uniques != null && uniques.length > 0) {\n\t\t\tlong exceptId = getLongValue(real, finalIdKey);\n\t\t\tMap<String,Object> map = new HashMap<>();\n\t\t\tfor (String u : uniques) {\n\t\t\t\tmap.put(u, real.get(u));\n\t\t\t}\n\t\t\tverifyRepeat(name, map, exceptId, finalIdKey, parser);\n\t\t}\n\t\t// 校验重复>>>>>>>>>>>>>>>>>>>\n\n\t\t// 校验并配置允许批量增删改部分失败<<<<<<<<<<<<<<<<<<<\n\t\tString allowPartialUpdateFail = StringUtil.get(getString(target, ALLOW_PARTIAL_UPDATE_FAIL.name()));\n\t\tString[] partialFails = StringUtil.split(allowPartialUpdateFail);\n\t\tif (partialFails != null && partialFails.length > 0) {\n\t\t\tfor (String key : partialFails) {\n                if (isArrayKey(key) == false) {\n                    throw new IllegalArgumentException(\"后端 Request 表中 \" + ALLOW_PARTIAL_UPDATE_FAIL.name()\n                            + \":value 中 \" + key + \" 不合法！必须以 [] 结尾！\");\n                }\n                if (target.get(key) instanceof Collection == false) {\n                    throw new IllegalArgumentException(\"后端 Request 表中 \" + ALLOW_PARTIAL_UPDATE_FAIL.name()\n                            + \":value 中 \" + key + \" 对应的 \" + key + \":[] 不存在！\");\n                }\n\n                // 可能 Table[] 和 Table:alias[] 冲突  int index = key.indexOf(\":\");\n                // String k = index < 0 ? key.substring(0, key.length() - 2) : key.substring(0, index);\n                String k = key.substring(0, key.length() - 2);\n                if (k.isEmpty()) {\n                    throw new IllegalArgumentException(\"后端 Request 表中 \" + ALLOW_PARTIAL_UPDATE_FAIL.name()\n                            + \":value 中 \" + key + \" 不合法！[] 前必须有名字！\");\n                }\n\n\t\t\t\tAbstractSQLConfig.ALLOW_PARTIAL_UPDATE_FAIL_TABLE_MAP.putIfAbsent(k, \"\");\n\t\t\t}\n\t\t}\n\t\t// 校验并配置允许部分批量增删改失败>>>>>>>>>>>>>>>>>>>\n\n\n\t\tString[] nks = ifObj == null ? null : StringUtil.split(getString(real, KEY_NULL));\n\t\tCollection<?> nkl = nks == null || nks.length <= 0 ? new HashSet<>() : Arrays.asList(nks);\n\n\t\tSet<Map.Entry<String, Object>> ifSet = ifObj == null ? null : ifObj.entrySet();\n\t\tif (ifIsStr || (ifSet != null && ifSet.isEmpty() == false)) {\n\t\t\t// 没必要限制，都是后端配置的，安全可控，而且可能确实有特殊需求，需要 id, @column 等\n//\t\t\tList<String> condKeys = new ArrayList<>(Arrays.asList(apijson.JSONMap.KEY_ID, apijson.JSONMap.KEY_ID_IN\n//\t\t\t\t\t, apijson.JSONMap.KEY_USER_ID, apijson.JSONMap.KEY_USER_ID_IN));\n//\t\t\tcondKeys.addAll(apijson.JSONMap.TABLE_KEY_LIST);\n\n\t\t\tString preCode = \"var curObj = \" + toJSONString(real) + \";\";\n\n\t\t\t// 未传的 key 在后面 eval 时总是报错 undefined，而且可能有冲突，例如对象里有 \"curObj\": val 键值对，就会覆盖当前对象定义，还不如都是 curObj.sex 这样取值\n//\t\t\tSet<Map.Entry<String, Object>> rset = real.entrySet();\n//\t\t\tfor (Map.Entry<String, Object> entry : rset) {\n//\t\t\t\tString k = entry == null ? null : entry.getKey();\n//\t\t\t\tif (StringUtil.isEmpty(k)) {\n//\t\t\t\t\tcontinue;\n//\t\t\t\t}\n//\t\t\t\tString vn = JSONResponse.formatOtherKey(k);\n//\t\t\t\tif (StringUtil.isName(vn) == false) { // 通过 curObj['id@'] 这样取值，写在 IF 配置里\n//\t\t\t\t\tcontinue;\n//\t\t\t\t}\n//\n//\t\t\t\tObject v = entry.getValue();\n//\t\t\t\tString vs = v instanceof String ? \"\\\"\" + ((String) v).replaceAll(\"\\\"\", \"\\\\\\\"\") + \"\\\"\"\n//\t\t\t\t\t\t: (JSON.isBooleanOrNumberOrString(v) ? v.toString() : JSON.format(v));\n//\t\t\t\tpreCode += \"\\nvar \" + vn + \" = \" + vs + \";\";\n//\t\t\t}\n\n\t\t\tif (ifIsStr) {\n\t\t\t\tString ifStr = (String) _if;\n\t\t\t\tint ind = ifStr.indexOf(\":\");\n\t\t\t\tString lang = ind < 0 || ind > 20 ? null : ifStr.substring(0, ind);\n\t\t\t\tboolean isName = StringUtil.isName(lang);\n\t\t\t\tScriptEngine engine = getScriptEngine(isName ? lang : null);\n\t\t\t\tengine.eval(preCode + \"\\n\" + (isName ? ifStr.substring(ind + 1) : ifStr));\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfor (Map.Entry<String, Object> entry : ifSet) {\n\t\t\t\t\tString k = entry == null ? null : entry.getKey();\n//\t\t\t\t\tif (condKeys.contains(k)) {\n//\t\t\t\t\t\tthrow new IllegalArgumentException(\"Request 表 structure 配置的 \" + ON.name()\n//\t\t\t\t\t\t\t\t+ \":{ \" + k + \":value } 中 key 不合法，不允许传 [\" + StringUtil.join(condKeys.toArray(new String[]{})) + \"] 中的任何一个 ！\");\n//\t\t\t\t\t}\n\n\t\t\t\t\tObject v = k == null ? null : entry.getValue();\n\t\t\t\t\tif (v instanceof String) {\n\t\t\t\t\t\tint ind = k.indexOf(\":\");\n\t\t\t\t\t\tString lang = ind < 0 || ind > 20 ? null : k.substring(0, ind);\n\t\t\t\t\t\tboolean isName = StringUtil.isName(lang);\n\t\t\t\t\t\tScriptEngine engine = getScriptEngine(isName ? lang : null);\n\t\t\t\t\t\tk = isName ? k.substring(ind + 1) : k;\n\n\t\t\t\t\t\tboolean isElse = StringUtil.isEmpty(k, false); // 其它直接报错，不允许传 StringUtil.isEmpty(k, true) || \"ELSE\".equals(k);\n//\t\t\t\t\t\tString code = preCode + \"\\n\\n\" + (StringUtil.isEmpty(v, false) ? k : (isElse ? v : \"if (\" + k + \") {\\n  \" + v + \"\\n}\"));\n\t\t\t\t\t\tString code = preCode + \"\\n\\n\" + (isElse ? v : \"if (\" + k + \") {\\n  \" + v + \"\\n}\");\n\n//\t\t\t\t\t\tScriptExecutor executor = new JavaScriptExecutor();\n//\t\t\t\t\t\texecutor.execute(null, real, )\n\n\t\t\t\t\t\tengine.eval(code);\n\n//\t\t\t\t\t\tPARSER_CREATOR.createFunctionParser()\n//\t\t\t\t\t\t\t\t.setCurrentObject(real)\n//\t\t\t\t\t\t\t\t.setKey(k)\n//\t\t\t\t\t\t\t\t.setMethod(method)\n//\t\t\t\t\t\t\t\t.invoke()\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (v instanceof Map<?, ?> == false) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(\"Request 表 structure 配置的 \" + IF.name()\n\t\t\t\t\t\t\t\t+ \":{ \" + k + \":value } 中 value 不合法，必须是 JSONRequest {} ！\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (nkl.contains(k) || real.get(k) != null) {\n\t\t\t\t\t\treal = parse(method, name, (M) v, real, database, schema, datasource, idCallback, parser, callback);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tLog.i(TAG, \"parse  return real = \" + toJSONString(real));\n\t\treturn real;\n\t}\n\n\tpublic static ScriptEngine getScriptEngine(String lang) {\n\t\tboolean isEmpty = StringUtil.isEmpty(lang, true);\n\t\tScriptEngine engine = isEmpty ? SCRIPT_ENGINE : SCRIPT_ENGINE_MANAGER.getEngineByName(lang);\n\n\t\tif (engine == null) {\n\t\t\tthrow new NullPointerException(\"找不到可执行 \" + (isEmpty ? \"js\" : lang) + \" 脚本的引擎！engine == null!\");\n\t\t}\n\n\t\treturn engine;\n\t}\n\n\n\t/**执行操作\n\t * @param opt\n\t * @param targetChild\n\t * @param real\n\t * @param parser\n\t * @return\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> M operate(Operation opt, M targetChild\n            , M real, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (targetChild == null) {\n\t\t\treturn real;\n\t\t}\n\t\tif (real == null) {\n\t\t\tthrow new IllegalArgumentException(\"operate  real == null!!!\");\n\t\t}\n\n\t\tSet<Map.Entry<String, Object>> set = new LinkedHashSet<>(targetChild.entrySet());\n\t\tfor (Map.Entry<String, Object> e : set) {\n\t\t\tString tk = e == null ? null : e.getKey();\n\t\t\tif (tk == null || OPERATION_KEY_LIST.contains(tk)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tObject tv = e.getValue();\n\n\t\t\tif (opt == TYPE) {\n\t\t\t\tverifyType(tk, tv, real);\n\t\t\t}\n\t\t\telse if (opt == VERIFY) {\n\t\t\t\tverifyValue(tk, tv, real, parser);\n\t\t\t}\n\t\t\telse if (opt == UPDATE) {\n\t\t\t\treal.put(tk, tv);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tif (real.containsKey(tk)) {\n\t\t\t\t\tif (opt == REPLACE) {\n\t\t\t\t\t\treal.put(tk, tv);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tif (opt == INSERT) {\n\t\t\t\t\t\treal.put(tk, tv);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn real;\n\t}\n\n\n\t/**验证值类型\n\t * @param tk\n\t * @param tv {@link Operation}\n\t * @param real\n\t * @throws Exception\n\t */\n\tpublic static void verifyType(@NotNull String tk, Object tv, @NotNull Map<String, Object> real)\n            throws UnsupportedDataTypeException {\n\t\tif (tv instanceof String == false) {\n\t\t\tthrow new UnsupportedDataTypeException(\"服务器内部错误，\" + tk + \":value 的value不合法！\"\n\t\t\t\t\t+ \"Request表校验规则中 TYPE:{ key:value } 中的value只能是String类型！\");\n\t\t}\n\n\t\tverifyType(tk, (String) tv, real.get(tk));\n\t}\n\t/**验证值类型\n\t * @param tk\n\t * @param tv {@link Operation}\n\t * @param rv\n\t * @throws Exception\n\t */\n\tpublic static void verifyType(@NotNull String tk, @NotNull String tv, Object rv)\n            throws UnsupportedDataTypeException {\n\t\tverifyType(tk, tv, rv, false);\n\t}\n\t/**验证值类型\n\t * @param tk\n\t * @param tv {@link Operation}\n\t * @param rv\n\t * @param isInArray\n\t * @throws Exception\n\t */\n\tpublic static void verifyType(@NotNull String tk, @NotNull String tv, Object rv, boolean isInArray)\n            throws UnsupportedDataTypeException {\n\t\tif (rv == null) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (tv.endsWith(\"[]\")) {\n\t\t\tverifyType(tk, \"ARRAY\", rv);\n\n\t\t\tfor (Object o : (Collection<?>) rv) {\n\t\t\t\tverifyType(tk, tv.substring(0, tv.length() - 2), o, true);\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\t//这里不抽取 enum，因为 enum 不能满足扩展需求，子类需要可以自定义，而且 URL[] 这种也不符合命名要求，得用 constructor + getter + setter\n\t\tswitch (tv) {\n\t\tcase \"BOOLEAN\": //Boolean.parseBoolean(getString(real, tk)); 只会判断null和true\n\t\t\tif (rv instanceof Boolean == false) { //apijson.JSONMap.getBoolean 可转换Number类型\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！类型必须是 BOOLEAN\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"NUMBER\": //整数\n\t\t\ttry {\n\t\t\t\tLong.parseLong(rv.toString()); //1.23会转换为1  real.getLong(tk);\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！类型必须是 NUMBER\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"DECIMAL\": //小数\n\t\t\ttry {\n\t\t\t\tDouble.parseDouble(rv.toString());\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是 DECIMAL\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"STRING\":\n\t\t\tif (rv instanceof String == false) { //apijson.JSONMap.getString 可转换任何类型\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是 STRING\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"URL\": //网址，格式为 http://www.apijson.org, https://www.google.com 等\n\t\t\ttry {\n\t\t\t\tnew URL((String) rv);\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是 URL\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"DATE\": //日期，格式为 YYYY-MM-DD（例如 2020-02-20）的 STRING\n\t\t\ttry {\n\t\t\t\tLocalDate.parse((String) rv);\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是格式为 YYYY-MM-DD（例如 2020-02-20）的 DATE\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"TIME\": //时间，格式为 HH:mm:ss（例如 12:01:30）的 STRING\n\t\t\ttry {\n\t\t\t\tLocalTime.parse((String) rv);\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是格式为 HH:mm:ss（例如 12:01:30）的 TIME\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"DATETIME\": //日期+时间，格式为 YYYY-MM-DDTHH:mm:ss（例如 2020-02-20T12:01:30）的 STRING\n\t\t\ttry {\n\t\t\t\tLocalDateTime.parse((String) rv);\n\t\t\t} catch (Exception e) {\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！类型必须是格式为 \" +\n                        \"YYYY-MM-DDTHH:mm:ss（例如 2020-02-20T12:01:30）的 DATETIME\" + (isInArray ? \"[] !\" : \" !\"));\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"OBJECT\":\n\t\t\tif (rv instanceof Map == false) { //apijson.JSONMap.getJSONObject 可转换String类型\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是 OBJECT\" + (isInArray ? \"[] !\" : \" !\") + \" OBJECT 结构为 {} !\");\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"ARRAY\":\n\t\t\tif (rv instanceof Collection == false) { //apijson.JSONMap.getJSONArray 可转换String类型\n\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！\" +\n                        \"类型必须是 ARRAY\" + (isInArray ? \"[] !\" : \" !\") + \" ARRAY 结构为 [] !\");\n\t\t\t}\n\t\t\tbreak;\n\t\t\t//目前在业务表中还用不上，单一的类型校验已经够用\n\t\t\t//\t\tcase \"JSON\":\n\t\t\t//\t\t\ttry {\n\t\t\t//\t\t\t\tcom.alibaba.fastjson.parseJSON(rv.toString());\n\t\t\t//\t\t\t} catch (Exception e) {\n\t\t\t//\t\t\t\tthrow new UnsupportedDataTypeException(tk + \":value 的value不合法！类型必须是 JSON ！\"\n\t\t\t//\t\t\t\t\t\t+ \"也就是 {Object}, [Array] 或 它们对应的字符串 '{Object}', '[Array]' 4种中的一个 !\");\n\t\t\t//\t\t\t}\n\t\t\t//\t\t\tbreak;\n\t\tdefault:\n\t\t\tthrow new UnsupportedDataTypeException(\n\t\t\t\t\t\"服务器内部错误，类型 \" + tv + \" 不合法！Request表校验规则中 TYPE:{ key:value } 中的 value 必须是\"\n\t\t\t\t\t\t\t+ \" [ BOOLEAN, NUMBER, DECIMAL, STRING, URL, DATE, TIME, DATETIME, OBJECT, ARRAY ] 或它们的数组\"\n\t\t\t\t\t\t\t+ \" [ BOOLEAN[], NUMBER[], DECIMAL[], STRING[], URL[], DATE[], TIME[], DATETIME[], OBJECT[], ARRAY[] ] 中的一个!\");\n\t\t}\n\n\t}\n\n\n\t/**验证值\n\t * @param tk\n\t * @param tv\n\t * @param real\n\t * @param parser\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyValue(@NotNull String tk\n\t\t\t, @NotNull Object tv, @NotNull M real, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (tv == null) {\n\t\t\tthrow new IllegalArgumentException(\"operate  operate == VERIFY \" + tk + \":\" + tv + \" ,  >> tv == null!!!\");\n\t\t}\n\n\t\tString rk;\n\t\tObject rv;\n\t\tLogic logic;\n\t\tif (tk.endsWith(\"$\")) {  // 模糊搜索\n\t\t\tverifyCondition(\"$\", real, tk, tv, parser);\n\t\t}\n\t\telse if (tk.endsWith(\"~\")) {  // 正则匹配\n\t\t\tlogic = new Logic(tk.substring(0, tk.length() - 1));\n\t\t\trk = logic.getKey();\n\t\t\trv = real.get(rk);\n\t\t\tif (rv == null) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tL array = AbstractSQLConfig.newJSONArray(tv);\n\n\t\t\tboolean m;\n\t\t\tboolean isOr = false;\n\t\t\tPattern reg;\n\t\t\tfor (Object r : array) {\n\t\t\t\tif (r instanceof String == false) {\n\t\t\t\t\tthrow new UnsupportedDataTypeException(rk + \":\" + rv + \" 中value只支持 String 或 [String] 类型！\");\n\t\t\t\t}\n\t\t\t\treg = COMPILE_MAP.get(r);\n\t\t\t\tif (reg == null) {\n\t\t\t\t\treg = Pattern.compile((String) r);\n\t\t\t\t}\n\t\t\t\tm = reg.matcher(\"\" + rv).matches();\n\t\t\t\tif (m) {\n\t\t\t\t\tif (logic.isNot()) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t\t}\n\t\t\t\t\tif (logic.isOr()) {\n\t\t\t\t\t\tisOr = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (logic.isAnd()) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isOr == false && logic.isOr()) {\n\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t}\n\t\t}\n\t\telse if (tk.endsWith(\"{}\")) { //rv符合tv条件或在tv内\n\t\t\tif (tv instanceof String) { //TODO  >= 0, < 10\n\t\t\t\tverifyCondition(\"{}\", real, tk, tv, parser);\n\t\t\t}\n\t\t\telse if (tv instanceof List<?>) {\n\t\t\t\tlogic = new Logic(tk.substring(0, tk.length() - 2));\n\t\t\t\trk = logic.getKey();\n\t\t\t\trv = real.get(rk);\n\t\t\t\tif (rv == null) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (((L) tv).contains(rv) == logic.isNot()) {\n\t\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t}\n\t\t\t}\n\t\t\telse {\n\t\t\t\tthrow new UnsupportedDataTypeException(\"服务器Request表verify配置错误！\");\n\t\t\t}\n\t\t}\n\t\telse if (tk.endsWith(\"<>\")) { //rv包含tv内的值\n\t\t\tlogic = new Logic(tk.substring(0, tk.length() - 2));\n\t\t\trk = logic.getKey();\n\t\t\trv = real.get(rk);\n\t\t\tif (rv == null) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (rv instanceof Collection<?> == false) {\n\t\t\t\tthrow new UnsupportedDataTypeException(\"服务器Request表verify配置错误！\");\n\t\t\t}\n\n\t\t\tL array = AbstractSQLConfig.newJSONArray(tv);\n\n\t\t\tboolean isOr = false;\n\t\t\tfor (Object o : array) {\n\t\t\t\tif (((L) rv).contains(o)) {\n\t\t\t\t\tif (logic.isNot()) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t\t}\n\t\t\t\t\tif (logic.isOr()) {\n\t\t\t\t\t\tisOr = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif (logic.isAnd()) {\n\t\t\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isOr == false && logic.isOr()) {\n\t\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t}\n\t\t}\n\t\telse {\n\t\t\tthrow new IllegalArgumentException(\"服务器Request表verify配置错误！\");\n\t\t}\n\t}\n\n\t/**校验字符串长度\n\t * @param rule\t规则\n\t * @param len\t长度\n\t * @return\n\t * @throws UnsupportedDataTypeException\n\t */\n\tpublic static boolean verifyLength(String rule, int len) throws UnsupportedDataTypeException {\n\t\tString first = null;\n\t\tString second = null;\n\t\tMatcher matcher = VERIFY_LENGTH_PATTERN.matcher(rule);\n\t\twhile (matcher.find()) {\n\t\t\tfirst = StringUtil.isEmpty(first) ? matcher.group(\"first\") : first;\n\t\t\tsecond = StringUtil.isEmpty(second) ? matcher.group(\"second\") : second;\n\t\t}\n\t\t// first和second为空表示规则不合法\n\t\tif (StringUtil.isEmpty(first) || StringUtil.isEmpty(second)) {\n\t\t\tthrow new UnsupportedDataTypeException(\"服务器Request表verify配置错误！\");\n\t\t}\n\n\t\tint secondNum = Integer.parseInt(second);\n\t\tswitch (Objects.requireNonNull(first)) {\n\t\t\tcase \">=\":\n\t\t\t\treturn len >= secondNum;\n\t\t\tcase \"<=\":\n\t\t\t\treturn len <= secondNum;\n\t\t\tcase \"!=\":\n\t\t\t\treturn len != secondNum;\n\t\t\tcase \">\":\n\t\t\t\treturn len > secondNum;\n\t\t\tcase \"<\":\n\t\t\t\treturn len < secondNum;\n\t\t\tcase \"=\":\n\t\t\t\treturn len == secondNum;\n\t\t}\n\n\t\t// 出现不能识别的符号也认为规则不合法\n\t\tthrow new UnsupportedDataTypeException(\"服务器Request表verify配置错误！\");\n\t}\n\n\t/**通过数据库执行SQL语句来验证条件\n\t * @param funChar\n\t * @param real\n\t * @param tk\n\t * @param tv\n\t * @param parser\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyCondition(\n\t\t\t@NotNull String funChar, @NotNull M real, @NotNull String tk, @NotNull Object tv\n\t\t\t, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\t// 不能用Parser, 0 这种不符合 StringUtil.isName !\n\n\t\tboolean isRange = \"{}\".equals(funChar);\n\n\t\tString k = tk.substring(0, tk.length() - funChar.length());\n\t\tLogic logic = new Logic(k);\n\t\tString rk = logic.getKey();\n\t\tboolean isLen = isRange && k.endsWith(\"[\");\n\t\tboolean isJSOnLen = isRange && k.endsWith(\"{\");\n\t\tk = isLen || isJSOnLen ? rk.substring(0, rk.length() - 1) : rk;\n\t\tObject rv = real.get(k);\n\t\tint len = 0;\n\t\tif (isLen) {\n\t\t\tlen = StringUtil.length(rv, false);\n\t\t}\n\t\telse if (isJSOnLen) {\n\t\t\trv = rv instanceof Map ? ((Map<?, ?>) rv) : (rv instanceof Collection ? ((Collection<?>) rv) : JSON.parse(rv));\n\t\t\tlen = rv instanceof Map ? ((Map<?, ?>) rv).size() : (rv instanceof Collection ? ((Collection<?>) rv).size() : StringUtil.length(rv, false));\n\t\t}\n\n\t\tif (isLen || isJSOnLen) {\n\t\t\tString[] tvs = tv.toString().split(\",\");\n\t\t\tfor (String tvItem : tvs) {\n\t\t\t\tif (! verifyLength(tvItem, len)) {\n\t\t\t\t\tthrow new IllegalArgumentException(k + \":value 中value长度不合法！必须匹配 \" + tk + \":\" + tv + \" !\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tif (rv == null) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (rv instanceof String && ((String) rv).contains(\"'\")) {  // || key.contains(\"#\") || key.contains(\"--\")) {\n\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！value 中不允许有单引号 ' ！\");\n\t\t}\n\n\t\tSQLConfig<T, M, L> config = parser.createSQLConfig().setMethod(GET).setCount(1).setPage(0);\n\t\tconfig.setTest(true);\n\t\t//\t\tconfig.setTable(Test.class.getSimpleName());\n\t\t//\t\tconfig.setColumn(rv + logic.getChar() + funChar)\n\t\t// 字符串可能 SQL 注入，目前的解决方式是加 TYPE 校验类型或者干脆不用 sqlVerify，而是通过远程函数来校验\n\t\tconfig.putWhere(rv + logic.getChar() + funChar, tv, false);\n\t\tconfig.setCount(1);\n\n\t\tSQLExecutor<T, M, L> executor = parser.createSQLExecutor(); // close 后复用导致不好修复的 NPE getSQLExecutor();\n\t\texecutor.setParser(parser);\n\t\tM result;\n\t\ttry {\n\t\t\tresult = executor.execute(config, false);\n\t\t} finally {\n\t\t\texecutor.close();\n\t\t}\n\n\t\tif (result != null && JSONResponse.isExist(result) == false) {\n\t\t\tthrow new IllegalArgumentException(rk + \":value 中value不合法！必须匹配 '\" + tk + \"': '\" + tv + \"' ！\");\n\t\t}\n\t}\n\n\n\t/**验证是否存在\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>>void verifyExist(String table, String key\n\t\t\t, Object value, long exceptId, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (key == null || value == null) {\n\t\t\tLog.e(TAG, \"verifyExist  key == null || value == null >> return;\");\n\t\t\treturn;\n\t\t}\n\t\tif (value instanceof JSON) {\n\t\t\tthrow new UnsupportedDataTypeException(key + \":value 中value的类型不能为JSON！\");\n\t\t}\n\t\tMap<String,Object> map = new HashMap<>();\n\t\tmap.put(key,value);\n\t\tverifyExist(table,map,exceptId,parser);\n\t}\n\n\t/**验证是否存在\n\t * @param table\n\t * @param param\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyExist(String table\n\t\t\t, Map<String,Object> param, long exceptId, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (param.isEmpty()) {\n\t\t\tLog.e(TAG, \"verifyExist is empty >> return;\");\n\t\t\treturn;\n\t\t}\n\n\t\tSQLConfig<T, M, L> config = parser.createSQLConfig().setMethod(HEAD).setCount(1).setPage(0);\n\t\tconfig.setTable(table);\n\t\tparam.forEach((key,value) -> config.putWhere(key, value, false));\n\n\t\tSQLExecutor<T, M, L> executor = parser.getSQLExecutor();\n\t\ttry {\n\t\t\tM result = executor.execute(config, false);\n\t\t\tif (result == null) {\n\t\t\t\tthrow new Exception(\"服务器内部错误  verifyExist  result == null\");\n\t\t\t}\n\t\t\tif (getIntValue(result, JSONResponse.KEY_COUNT) <= 0) {\n\t\t\t\tStringBuilder sb = new StringBuilder();\n\t\t\t\tparam.forEach((key,value) -> sb.append(\"key:\").append(key).append(\" value:\").append(value).append(\" \"));\n\t\t\t\tthrow new ConflictException(sb + \"的数据不存在！如果必要请先创建！\");\n\t\t\t}\n\t\t} finally {\n\t\t\texecutor.close();\n\t\t}\n\t}\n\n\t/**验证是否重复\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyRepeat(String table, String key\n\t\t\t, Object value, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tverifyRepeat(table, key, value, 0, parser);\n\t}\n\n\t/**验证是否重复\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @param exceptId 不包含id\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyRepeat(String table, String key\n\t\t\t, Object value, long exceptId, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tverifyRepeat(table, key, value, exceptId, null, parser);\n\t}\n\n\t/**验证是否重复\n\t * TODO 与 AbstractVerifier.verifyRepeat 代码重复，需要简化\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @param exceptId 不包含id\n\t * @param idKey\n\t * @param parser\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>>void verifyRepeat(String table, String key\n\t\t\t, Object value, long exceptId, String idKey, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (key == null || value == null) {\n\t\t\tLog.e(TAG, \"verifyRepeat  key == null || value == null >> return;\");\n\t\t\treturn;\n\t\t}\n\t\tif (value instanceof JSON) {\n\t\t\tthrow new UnsupportedDataTypeException(key + \":value 中value的类型不能为JSON！\");\n\t\t}\n\t\tMap<String,Object> map = new HashMap<>();\n\t\tmap.put(key,value);\n\t\tverifyRepeat(table, map, exceptId, idKey, parser);\n\t}\n\n\t/**验证是否重复\n\t * TODO 与 AbstractVerifier.verifyRepeat 代码重复，需要简化\n\t * @param table\n\t * @param param\n\t * @param exceptId 不包含id\n\t * @param idKey\n\t * @param parser\n\t * @throws Exception\n\t */\n\tpublic static <T, M extends Map<String, Object>, L extends List<Object>> void verifyRepeat(String table\n\t\t\t, Map<String,Object> param, long exceptId, String idKey, @NotNull Parser<T, M, L> parser) throws Exception {\n\t\tif (param.isEmpty()) {\n\t\t\tLog.e(TAG, \"verifyRepeat is empty >> return;\");\n\t\t\treturn;\n\t\t}\n\n\t\tString finalIdKey = StringUtil.isEmpty(idKey, false) ? KEY_ID : idKey;\n\n\t\tSQLConfig<T, M, L> config = parser.createSQLConfig().setMethod(HEAD).setCount(1).setPage(0);\n\t\tconfig.setTable(table);\n\t\tif (exceptId > 0) { //允许修改自己的属性为该属性原来的值\n\t\t\tconfig.putWhere(finalIdKey + \"!\", exceptId, false);\n\t\t}\n\t\tparam.forEach((key,value) -> config.putWhere(key,value, false));\n\n\t\tSQLExecutor<T, M, L> executor = parser.getSQLExecutor();\n\t\ttry {\n\t\t\tM result = executor.execute(config, false);\n\t\t\tif (result == null) {\n\t\t\t\tthrow new Exception(\"服务器内部错误  verifyRepeat  result == null\");\n\t\t\t}\n\t\t\tif (getIntValue(result, JSONResponse.KEY_COUNT) > 0) {\n\t\t\t\tStringBuilder sb = new StringBuilder();\n\t\t\t\tparam.forEach((key,value) -> sb.append(\"key:\").append(key).append(\" value:\").append(value).append(\" \"));\n\t\t\t\tthrow new ConflictException(sb + \"的数据已经存在，不能重复！\");\n\t\t\t}\n\t\t} finally {\n\t\t\texecutor.close();\n\t\t}\n\t}\n\n\n\tpublic static String getCacheKeyForRequest(String method, String tag) {\n\t\treturn method + \"/\" + tag;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Entry.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.Map;\n\n/**自定义Entry\n * *java.util.Map.Entry是interface，new Entry(...)不好用，其它的Entry也不好用\n * @author Lemon\n * @param <K> key\n * @param <V> value\n * @use new Entry<K, V>(...)\n * @warn K,V都需要基本类型时不建议使用，判空麻烦，不如新建一个Model\n */\npublic class Entry<K, V> implements Map.Entry<K, V> {\n\n\tpublic K key;\n\tpublic V value;\n\n\tpublic Entry() {\n\t\t//default\n\t}\n\tpublic Entry(K key) {\n\t\tthis(key, null);\n\t}\n\tpublic Entry(K key, V value) {\n\t\tthis.key = key;\n\t\tthis.value = value;\n\t}\n\n\n\tpublic K getKey() {\n\t\treturn key;\n\t}\n\tpublic void setKey(K key) {\n\t\tthis.key = key;\n\t}\n\tpublic V getValue() {\n\t\treturn value;\n\t}\n\tpublic V setValue(V value) {\n\t\tthis.value = value;\n\t\treturn value;\n\t}\n\n\tpublic boolean isEmpty() {\n\t\treturn key == null && value == null;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/FunctionParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.*;\n\n\n/**远程函数解析器\n * @author Lemon\n */\npublic interface FunctionParser<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\tObject invoke(@NotNull String function, @NotNull M currentObject) throws Exception;\n\tObject invoke(@NotNull String function, @NotNull M currentObject, boolean containRaw) throws Exception;\n\n\tParser<T, M, L> getParser();\n\n\tFunctionParser<T, M, L> setParser(Parser<T, M, L> parser);\n\n\tRequestMethod getMethod();\n\tFunctionParser<T, M, L> setMethod(RequestMethod method);\n\n\tString getTag();\n\tFunctionParser<T, M, L> setTag(String tag);\n\n\tint getVersion();\n\tFunctionParser<T, M, L> setVersion(int version);\n\n\t@NotNull \n\tM getRequest();\n\tFunctionParser<T, M, L> setRequest(@NotNull M request);\n\n\n\tString getKey();\n\tFunctionParser<T, M, L> setKey(String key);\n\t\n\tString getParentPath();\n\tFunctionParser<T, M, L> setParentPath(String parentPath);\n\n\tString getCurrentName();\n\tFunctionParser<T, M, L> setCurrentName(String currentName);\n\n\n\t@NotNull\n\tM getCurrentObject();\n\tFunctionParser<T, M, L> setCurrentObject(@NotNull M currentObject);\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/JSONRequest.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.*;\n\nimport apijson.JSON;\nimport apijson.Log;\nimport apijson.StringUtil;\n\n/**JSONRequest for Server to replace apijson.JSONMap,\n * put JSON.parseObject(value) and not encode in public cases\n * @author Lemon\n * @see #put(String, Object)\n */\npublic class JSONRequest implements apijson.JSONRequest<LinkedHashMap<String, Object>, ArrayList<Object>> {\n\n\tprotected Map<String, Object> map = new LinkedHashMap<>();\n\tpublic JSONRequest() {\n\t\tsuper();\n\t}\n\t/**\n\t * encode = true\n\t * {@link #JSONRequest(String, Object)}\n\t * @param object\n\t */\n\tpublic JSONRequest(Object object) {\n\t\tsuper();\n\t\tput(object);\n\t}\n\t/**\n\t * @param name\n\t * @param object\n\t */\n\tpublic JSONRequest(String name, Object object) {\n\t\tsuper();\n\t\tput(name, object);\n\t}\n\n\t///**create a parent JSONMap named KEY_ARRAY\n\t// * @param count\n\t// * @param page\n\t// * @return {@link #toArray(int, int)}\n\t// */\n\t//public LinkedHashMap<String, Object> toArray(int count, int page) {\n\t//\treturn toArray(count, page, null);\n\t//}\n\t//\n\t///**create a parent JSONMap named name+KEY_ARRAY.\n\t// * @param count\n\t// * @param page\n\t// * @param name\n\t// * @return {name+KEY_ARRAY : this}. if needs to be put, use {@link #putsAll(Map<? extends String, ? extends Object>)} instead\n\t// */\n\t//public LinkedHashMap<String, Object> toArray(int count, int page, String name) {\n\t//\treturn new JSONRequest(StringUtil.get(name) + KEY_ARRAY, this.setCount(count).setPage(page));\n\t//}\n\n\t@Override\n\tpublic JSONRequest putsAll(Map<? extends String, ? extends Object> m) {\n\t\tputAll(m);\n\t\treturn this;\n\t}\n\n\t/**\n\t * @param value\n\t * @return {@link #puts(String, Object)}\n\t */\n\t@Override\n\tpublic JSONRequest puts(Object value) {\n\t\tput(value);\n\t\treturn this;\n\t}\n\t/**\n\t * @param key\n\t * @param value\n\t * @return this\n\t * @see {@link #put(String, Object)}\n\t */\n\t@Override\n\tpublic JSONRequest puts(String key, Object value) {\n\t\tput(key, value);\n\t\treturn this;\n\t}\n\n\n\t/**自定义类型必须转为JSONObject或JSONArray，否则RequestParser解析不了\n\t */\n\t@Override\n\tpublic Object put(String key, Object value) {\n\t\tif (value == null) {//  || key == null\n\t\t\treturn null;\n\t\t}\n\n        Object target = null;\n        try {\n            target = JSON.parse(value);\n        } catch (Exception e) {\n            // nothing\n\t\t\tLog.e(TAG, \"JSON.parse failed for key: \" + key, e);\n        }\n        //\t\tif (target == null) { // \"tag\":\"User\" 报错\n\t\t//\t\t\treturn null;\n\t\t//\t\t}\n\t\treturn map.put(StringUtil.isNotEmpty(key, true) ? key : value.getClass().getSimpleName() //must handle key here\n\t\t\t\t, target == null ? value : target);\n\t}\n\n\n\t@Override\n\tpublic int size() {\n\t\treturn map.size();\n\t}\n\n\t@Override\n\tpublic boolean isEmpty() {\n\t\treturn map.isEmpty();\n\t}\n\n\t@Override\n\tpublic boolean containsKey(Object key) {\n\t\treturn map.containsKey(key);\n\t}\n\n\t@Override\n\tpublic boolean containsValue(Object value) {\n\t\treturn map.containsValue(value);\n\t}\n\n\t@Override\n\tpublic Object get(Object key) {\n\t\treturn map.get(key);\n\t}\n\n\t@Override\n\tpublic Object remove(Object key) {\n\t\treturn map.remove(key);\n\t}\n\n\n\t@Override\n\tpublic void clear() {\n\t\tmap.clear();\n\t}\n\n\t@Override\n\tpublic Set<String> keySet() {\n\t\treturn map.keySet();\n\t}\n\n\t@Override\n\tpublic Collection<Object> values() {\n\t\treturn map.values();\n\t}\n\n\t@Override\n\tpublic Set<Entry<String, Object>> entrySet() {\n\t\treturn map.entrySet();\n\t}\n\n\t@Override\n\tpublic String toString() {\n\t\treturn JSON.toJSONString(map);\n\t}\n\n\tpublic String toJSONString() {\n\t\treturn JSON.toJSONString(map);\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Join.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.NotNull;\nimport apijson.StringUtil;\n\n/**连表 配置\n * @author Lemon\n */\npublic class Join<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\tprivate String path;  // /User/id@\n\n\tprivate String joinType;  // \"@\" - APP, \"<\" - LEFT, \">\" - RIGHT, \"*\" - CROSS, \"&\" - INNER, \"|\" - FULL, \"!\" - OUTER, \"^\" - SIDE, \"(\" - ANTI, \")\" - FOREIGN, \"~\" ASOF\n\tprivate String table;  // User\n\tprivate String alias;  // owner\n\tprivate int count = 1;\t// 当app join子表，需要返回子表的行数，默认1行；\n\tprivate List<On> onList;  // ON User.id = Moment.userId AND ...\n\n\tprivate M request;  // { \"id@\":\"/Moment/userId\" }\n\tprivate M on;  // \"join\": { \"</User\": { \"@order\":\"id-\", \"@group\":\"id\", \"name~\":\"a\", \"tag$\":\"%a%\", \"@combine\": \"name~,tag$\" } } 中的 </User 对应值\n\n\tprivate M outer; // \"join\": { \"</User\": { \"key2\": value2, \"@column\": \"key2,key3\",\"@group\": \"key2+\" }}\n\tprivate SQLConfig<T, M, L> joinConfig;\n\tprivate SQLConfig<T, M, L> cacheConfig;\n\tprivate SQLConfig<T, M, L> onConfig;\n\n\tprivate SQLConfig<T, M, L> outerConfig;\n\n\tpublic String getPath() {\n\t\treturn path;\n\t}\n\tpublic void setPath(String path) {\n\t\tthis.path = path;\n\t}\n\n\tpublic int getCount() {\n\t\treturn count;\n\t}\n\tpublic void setCount(int count) {\n\t\tthis.count = count;\n\t}\n\n\tpublic String getJoinType() {\n\t\treturn joinType;\n\t}\n\tpublic void setJoinType(String joinType) {\n\t\tthis.joinType = joinType;\n\t}\n\tpublic String getTable() {\n\t\treturn table;\n\t}\n\tpublic void setTable(String table) {\n\t\tthis.table = table;\n\t}\n\tpublic String getAlias() {\n\t\treturn alias;\n\t}\n\tpublic void setAlias(String alias) {\n\t\tthis.alias = alias;\n\t}\n\n\tpublic List<On> getOnList() {\n\t\treturn onList;\n\t}\n\tpublic void setOnList(List<On> onList) {\n\t\tthis.onList = onList;\n\t}\n\n\tpublic M getRequest() {\n\t\treturn request;\n\t}\n\tpublic void setRequest(M request) {\n\t\tthis.request = request;\n\t}\n\tpublic M getOn() {\n\t\treturn on;\n\t}\n\tpublic void setOn(M on) {\n\t\tthis.on = on;\n\t}\n\n\tpublic void setOuter(M outer) {\n\t\tthis.outer = outer;\n\t}\n\n\tpublic M getOuter() {\n\t\treturn outer;\n\t}\n\n\tpublic SQLConfig<T, M, L> getOuterConfig() {\n\t\treturn outerConfig;\n\t}\n\n\tpublic void setOuterConfig(SQLConfig<T, M, L> outerConfig) {\n\t\tthis.outerConfig = outerConfig;\n\t}\n\n\tpublic SQLConfig<T, M, L> getJoinConfig() {\n\t\treturn joinConfig;\n\t}\n\tpublic void setJoinConfig(SQLConfig<T, M, L> joinConfig) {\n\t\tthis.joinConfig = joinConfig;\n\t}\n\tpublic SQLConfig<T, M, L> getCacheConfig() {\n\t\treturn cacheConfig;\n\t}\n\tpublic void setCacheConfig(SQLConfig<T, M, L> cacheConfig) {\n\t\tthis.cacheConfig = cacheConfig;\n\t}\n\tpublic SQLConfig<T, M, L> getOnConfig() {\n\t\treturn onConfig;\n\t}\n\tpublic void setOnConfig(SQLConfig<T, M, L> onConfig) {\n\t\tthis.onConfig = onConfig;\n\t}\n\n\tpublic boolean isOne2One() {\n\t\treturn ! isOne2Many();\n\t}\n\tpublic boolean isOne2Many() {\n\t\treturn count != 1 || (path != null && path.contains(\"[]\"));  // TODO 必须保证一对一时不会传包含 [] 的 path\n\t}\n\n\tpublic boolean isAppJoin() {\n\t\treturn \"@\".equals(getJoinType());\n\t}\n\tpublic boolean isLeftJoin() {\n\t\treturn \"<\".equals(getJoinType());\n\t}\n\tpublic boolean isRightJoin() {\n\t\treturn \">\".equals(getJoinType());\n\t}\n\tpublic boolean isCrossJoin() {\n\t\treturn \"*\".equals(getJoinType());\n\t}\n\tpublic boolean isInnerJoin() {\n\t\treturn \"&\".equals(getJoinType());\n\t}\n\tpublic boolean isFullJoin() {\n\t\tString jt = getJoinType();\n\t\treturn \"\".equals(jt) || \"|\".equals(jt);\n\t}\n\tpublic boolean isOuterJoin() {\n\t\treturn \"!\".equals(getJoinType());\n\t}\n\tpublic boolean isSideJoin() {\n\t\treturn \"^\".equals(getJoinType());\n\t}\n\tpublic boolean isAntiJoin() {\n\t\treturn \"(\".equals(getJoinType());\n\t}\n\tpublic boolean isForeignJoin() {\n\t\treturn \")\".equals(getJoinType());\n\t}\n\tpublic boolean isAsofJoin() {\n\t\treturn \"~\".equals(getJoinType());\n\t}\n\n\tpublic boolean isLeftOrRightJoin() {\n\t\tString jt = getJoinType();\n\t\treturn \"<\".equals(jt) || \">\".equals(jt);\n\t}\n\n\tpublic boolean canCacheViceTable() {\n\t\tString jt = getJoinType();\n\t\treturn \"@\".equals(jt) || \"<\".equals(jt) || \">\".equals(jt) || \"&\".equals(jt) || \"*\".equals(jt) || \")\".equals(jt);\n\t\t// 副表是按常规条件查询，缓存会导致其它同表同条件对象查询结果集为空\t\treturn ! isFullJoin();  // ! - OUTER, ( - FOREIGN 都需要缓存空副表数据，避免多余的查询\n\t}\n\n\tpublic boolean isSQLJoin() {\n\t\treturn ! isAppJoin();\n\t}\n\n\tpublic static boolean isSQLJoin(Join<?, ?, ?> j) {\n\t\treturn j != null && j.isSQLJoin();\n\t}\n\n\tpublic static boolean isAppJoin(Join<?, ?, ?> j) {\n\t\treturn j != null && j.isAppJoin();\n\t}\n\n\tpublic static boolean isLeftOrRightJoin(Join<?, ?, ?> j) {\n\t\treturn j != null && j.isLeftOrRightJoin();\n\t}\n\n\n\n\tpublic static class On {\n\n\t\tprivate String originKey;\n\t\tprivate String originValue;\n\n\t\tprivate Logic logic; // & | !\n\t\tprivate String relateType;  // \"\" - 一对一, \"{}\" - 一对多, \"<>\" - 多对一, > , <= , !=\n\t\tprivate String key;  // id\n\t\tprivate String targetTableKey;  // Moment:main\n\t\tprivate String targetTable;  // Moment\n\t\tprivate String targetAlias;  // main\n\t\tprivate String targetKey;  // userId\n\n\t\tpublic String getOriginKey() {\n\t\t\treturn originKey;\n\t\t}\n\t\tpublic void setOriginKey(String originKey) {\n\t\t\tthis.originKey = originKey;\n\t\t}\n\t\tpublic String getOriginValue() {\n\t\t\treturn originValue;\n\t\t}\n\t\tpublic void setOriginValue(String originValue) {\n\t\t\tthis.originValue = originValue;\n\t\t}\n\n\n\t\tpublic Logic getLogic() {\n\t\t\treturn logic;\n\t\t}\n\t\tpublic void setLogic(Logic logic) {\n\t\t\tthis.logic = logic;\n\t\t}\n\t\tpublic String getRelateType() {\n\t\t\treturn relateType;\n\t\t}\n\t\tpublic void setRelateType(String relateType) {\n\t\t\tthis.relateType = relateType;\n\t\t}\n\n\t\tpublic String getKey() {\n\t\t\treturn key;\n\t\t}\n\t\tpublic void setKey(String key) {\n\t\t\tthis.key = key;\n\t\t}\n\n\t\tpublic void setTargetTableKey(String targetTableKey) {\n\t\t\tthis.targetTableKey = targetTableKey;\n\t\t}\n\t\tpublic String getTargetTableKey() {\n\t\t\treturn targetTableKey;\n\t\t}\n\n\t\tpublic void setTargetTable(String targetTable) {\n\t\t\tthis.targetTable = targetTable;\n\t\t}\n\t\tpublic String getTargetTable() {\n\t\t\treturn targetTable;\n\t\t}\n\t\tpublic void setTargetAlias(String targetAlias) {\n\t\t\tthis.targetAlias = targetAlias;\n\t\t}\n\t\tpublic String getTargetAlias() {\n\t\t\treturn targetAlias;\n\t\t}\n\t\tpublic String getTargetKey() {\n\t\t\treturn targetKey;\n\t\t}\n\t\tpublic void setTargetKey(String targetKey) {\n\t\t\tthis.targetKey = targetKey;\n\t\t}\n\n\n\t\tpublic void setKeyAndType(String joinType, String table, @NotNull String originKey) throws Exception { //id, id@, id{}@, contactIdList<>@ ...\n\t\t\tif (originKey.endsWith(\"@\")) {\n\t\t\t\toriginKey = originKey.substring(0, originKey.length() - 1);\n\t\t\t}\n\t\t\telse { //TODO 暂时只允许 User.id = Moment.userId 字段关联，不允许 User.id = 82001 这种\n\t\t\t\tthrow new IllegalArgumentException(joinType + \"/.../\" + table + \"/\" + originKey + \" 中字符 \" + originKey + \" 不合法！join:'.../refKey'\" + \" 中 refKey 必须以 @ 结尾！\");\n\t\t\t}\n\n\t\t\tString k;\n\n\t\t\tif (originKey.endsWith(\"{}\")) {\n\t\t\t\tsetRelateType(\"{}\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 2);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\"<>\")) {\n\t\t\t\tsetRelateType(\"<>\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 2);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\"$\")) {  // key%$:\"a\" -> key LIKE '%a%'; key?%$:\"a\" -> key LIKE 'a%'; key_?$:\"a\" -> key LIKE '_a'; key_%$:\"a\" -> key LIKE '_a%'\n\t\t\t\tk = originKey.substring(0, originKey.length() - 1);\n\t\t\t\tchar c = k.isEmpty() ? 0 : k.charAt(k.length() - 1);\n\n\t\t\t\tString t = \"$\";\n\t\t\t\tif (c == '%' || c == '_' || c == '?') {\n\t\t\t\t\tt = c + t;\n\t\t\t\t\tk = k.substring(0, k.length() - 1);\n\n\t\t\t\t\tchar c2 = k.isEmpty() ? 0 : k.charAt(k.length() - 1);\n\t\t\t\t\tif (c2 == '%' || c2 == '_' || c2 == '?') {\n\t\t\t\t\t\tif (c2 == c) {\n\t\t\t\t\t\t\tthrow new IllegalArgumentException(originKey + \":value 中字符 \" + k + \" 不合法！key$:value 中不允许 key 中有连续相同的占位符！\");\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tt = c2 + t;\n\t\t\t\t\t\tk = k.substring(0, k.length() - 1);\n\t\t\t\t\t}\n\t\t\t\t\telse if (c == '?') {\n\t\t\t\t\t\tthrow new IllegalArgumentException(originKey + \":value 中字符 \" + originKey + \" 不合法！key$:value 中不允许只有单独的 '?'，必须和 '%', '_' 之一配合使用 ！\");\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsetRelateType(t);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\"~\")) {\n\t\t\t\tboolean ignoreCase = originKey.endsWith(\"*~\");\n\t\t\t\tsetRelateType(ignoreCase ? \"*~\" : \"~\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - (ignoreCase ? 2 : 1));\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\">=\")) {\n\t\t\t\tsetRelateType(\">=\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 2);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\"<=\")) {\n\t\t\t\tsetRelateType(\"<=\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 2);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\">\")) {\n\t\t\t\tsetRelateType(\">\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 1);\n\t\t\t}\n\t\t\telse if (originKey.endsWith(\"<\")) {\n\t\t\t\tsetRelateType(\"<\");\n\t\t\t\tk = originKey.substring(0, originKey.length() - 1);\n\t\t\t}\n\t\t\telse {\n\t\t\t\tsetRelateType(\"\");\n\t\t\t\tk = originKey;\n\t\t\t}\n\n\t\t\tif (k != null && (k.contains(\"&\") || k.contains(\"|\"))) {\n\t\t\t\tthrow new UnsupportedOperationException(joinType + \"/.../\" + table + \"/\" + originKey + \" 中字符 \" + k + \" 不合法！与或非逻辑符仅支持 '!' 非逻辑符 ！\");\n\t\t\t}\n\n\t\t\t//TODO if (c3 == '-') { // 表示 key 和 value 顺序反过来: value LIKE key\n\n\t\t\tLogic l = new Logic(k);\n\t\t\tsetLogic(l);\n\n\t\t\tif (StringUtil.isName(l.getKey()) == false) {\n\t\t\t\tthrow new IllegalArgumentException(joinType + \"/.../\" + table + \"/\" + originKey + \" 中字符 \" + l.getKey() + \" 不合法！必须符合字段命名格式！\");\n\t\t\t}\n\n\t\t\tsetKey(l.getKey());\n\t\t}\n\n  }\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Logic.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.StringUtil;\n\n/**& | !逻辑\n * @author Lemon\n */\npublic class Logic {\n\n\tpublic static final int TYPE_OR = 0;\n\tpublic static final int TYPE_AND = 1;\n\tpublic static final int TYPE_NOT = 2;\n\tpublic static final int[] TYPES = {TYPE_OR, TYPE_AND, TYPE_NOT};\n\n\tpublic static final String CHAR_OR = \"|\";\n\tpublic static final String CHAR_AND = \"&\";\n\tpublic static final String CHAR_NOT = \"!\";\n\tpublic static final String[] CHARS = {CHAR_OR, CHAR_AND, CHAR_NOT};\n\n\tpublic static final String NAME_OR = \"OR\";\n\tpublic static final String NAME_AND = \"AND\";\n\tpublic static final String NAME_NOT = \"NOT\";\n\tpublic static final String[] NAMES = {NAME_OR, NAME_AND, NAME_NOT};\n\n\n\tprivate int type;\n\tprivate String key;\n\tprivate String originKey;\n\n\tpublic Logic() {\n\t\tsuper();\n\t}\n\n\tpublic Logic(int type) {\n\t\tthis();\n\t\tthis.type = type;\n\t}\n\tpublic Logic(String key) {\n\t\tthis.originKey = key;\n\t\tkey = StringUtil.get(key);\n\n\t\tint type = getType(key.isEmpty() ? \"\" : key.substring(key.length() - 1));\n\n\t\tif (type >= 0 && type <= 2) {\n\t\t\tkey = key.substring(0, key.length() - 1);\n\t\t}\n\t\tif (type < 0) {\n\t\t\ttype = 0;\n\t\t}\n\n\n\t\tsetType(type);\n\t\tsetKey(key);\n\t}\n\n\n\tpublic int getType() {\n\t\treturn type;\n\t}\n\n\tpublic void setType(int type) {\n\t\tthis.type = type;\n\t}\n\n\tpublic String getKey() {\n\t\treturn key;\n\t}\n\tpublic void setKey(String key) {\n\t\tthis.key = key;\n\t}\n\tpublic String getOriginKey() {\n\t\treturn originKey;\n\t}\n\n\n\tpublic boolean isOr() {\n\t\treturn isOr(type);\n\t}\n\tpublic static boolean isOr(int type) {\n\t\treturn type == TYPE_OR;\n\t}\n\tpublic boolean isAnd() {\n\t\treturn isAnd(type);\n\t}\n\tpublic static boolean isAnd(int type) {\n\t\treturn type == TYPE_AND;\n\t}\n\tpublic boolean isNot() {\n\t\treturn isNot(type);\n\t}\n\tpublic static boolean isNot(int type) {\n\t\treturn type == TYPE_NOT;\n\t}\n\t\n\tpublic boolean isCorrect() {\n\t\treturn isContain(getType());\n\t}\n\tpublic static boolean isContain(String s) {\n\t\treturn isContain(getType(s));\n\t}\n\tpublic static boolean isContain(int type) {\n\t\treturn type >= TYPE_OR && type <= TYPE_NOT;\n\t}\n\n\tpublic static int getType(char logicChar) {\n\t\treturn getType(String.valueOf(logicChar));\n\t}\n\tpublic static int getType(String logicChar) {\n\t\tint type = -1;\n\t\tif (logicChar != null && logicChar.length() == 1) {\n\t\t\tswitch (logicChar) {\n\t\t\t\tcase \"|\":\n\t\t\t\t\ttype = 0;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"&\":\n\t\t\t\t\ttype = 1;\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"!\":\n\t\t\t\t\ttype = 2;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\treturn type;\n\t}\n\n\tpublic String getChar() {\n\t\treturn getChar(type);\n\t}\n\tpublic static String getChar(int type) {\n\t\treturn type < 0 || type >= CHARS.length ? \"\" : CHARS[type];\n\t}\n\n\tpublic String getName() {\n\t\treturn getName(type);\n\t}\n\tpublic static String getName(int type) {\n\t\treturn type < 0 || type >= NAMES.length ? \"\" : NAMES[type];\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/ObjectParser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.*;\n\n/**简化Parser，getObject和getArray(getArrayConfig)都能用\n * @author Lemon\n */\npublic interface ObjectParser<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\tParser<T, M, L> getParser();\n\tObjectParser<T, M, L> setParser(Parser<T, M, L> parser);\n\n\tString getParentPath();\n\tObjectParser<T, M, L> setParentPath(String parentPath);\n\n\tObjectParser<T, M, L> setCache(M cache);\n\tM getCache();\n\n\n\t/**解析成员\n\t * response重新赋值\n\t * @param name\n\t * @param isReuse \n\t * @return null or this\n\t * @throws Exception\n\t */\n\tObjectParser<T, M, L> parse(String name, boolean isReuse) throws Exception;\n\n\t/**调用 parser 的 sqlExecutor 来解析结果\n\t * @param method\n\t * @param table\n\t * @param alias\n\t * @param request\n\t * @param joinList\n\t * @param isProcedure\n\t * @return\n\t * @throws Exception\n\t */\n\tM parseResponse(RequestMethod method, String table, String alias, M request, List<Join<T, M, L>> joinList, boolean isProcedure) throws Exception;\n\t/**调用 parser 的 sqlExecutor 来解析结果\n\t * @param config\n\t * @param isProcedure\n\t * @return\n\t * @throws Exception\n\t */\n\tM parseResponse(SQLConfig<T, M, L> config, boolean isProcedure) throws Exception;\n\n\n\n\t/**解析普通成员\n\t * @param key\n\t * @param value\n\t * @return whether parse succeed\n\t */\n\tboolean onParse(@NotNull String key, @NotNull Object value) throws Exception;\n\n\t/**解析子对象\n\t * @param index\n\t * @param key\n\t * @param value\n\t * @param cache SQL 结果缓存\n\t * @return\n\t * @throws Exception\n\t */\n\tObject onChildParse(int index, String key, M value, Object cache) throws Exception;\n\t\n\t/**解析赋值引用\n\t * @param path\n\t * @return\n\t */\n\tObject onReferenceParse(@NotNull String path);\n\n\t//TODO 改用 MySQL json_add,json_remove,json_contains 等函数！ \n\t/**修改数组 PUT key:[]\n\t * @param key\n\t * @param array\n\t * @throws Exception\n\t */\n\tvoid onPUTArrayParse(@NotNull String key, @NotNull L array) throws Exception;\n\n\t/**批量新增或修改 POST or PUT  Table[]:[{}]\n\t * @param key\n\t * @param array\n\t * @throws Exception\n\t */\n\tvoid onTableArrayParse(@NotNull String key, @NotNull L array) throws Exception;\n\n\t/**SQL 配置，for single object\n\t * @return {@link #setSQLConfig(int, int, int)}\n\t * @throws Exception\n\t */\n\tObjectParser<T, M, L> setSQLConfig() throws Exception;\n\n\t/**SQL 配置\n\t * @return \n\t * @throws Exception\n\t */\n\tObjectParser<T, M, L> setSQLConfig(int count, int page, int position) throws Exception;\n\t\n\t\n\t/**执行 SQL\n\t * @return \n\t * @throws Exception\n\t */\n\tObjectParser<T, M, L> executeSQL() throws Exception;\n\n\t\n\t/**\n\t * @return\n\t * @throws Exception\n\t */\n\tM onSQLExecute() throws Exception;\n\t\n\t\n\t/**\n\t * @return response\n\t * @throws Exception\n\t */\n\tM response() throws Exception;\n\n\tvoid onFunctionResponse(String type) throws Exception;\n\n\tvoid onChildResponse() throws Exception;\n\t\n\n\tSQLConfig<T, M, L> newSQLConfig(boolean isProcedure) throws Exception;\n\tSQLConfig<T, M, L> newSQLConfig(RequestMethod method, String table, String alias, M request, List<Join<T, M, L>> joinList, boolean isProcedure) throws Exception;\n\t\n\t/**\n\t * response has the final value after parse (and query if isTableKey)\n\t */\n\tvoid onComplete();\n\n\n\t/**回收内存\n\t */\n\tvoid recycle();\n\n\n\tObjectParser<T, M, L> setMethod(RequestMethod method);\n\tRequestMethod getMethod();\n\n\n\tboolean isTable();\n\tString getPath();\n\tString getTable();\n\tString getAlias();\n\tSQLConfig<T, M, L> getArrayConfig();\n\n\tSQLConfig<T, M, L> getSQLConfig();\n\tM getResponse();\n\tM getSQLRequest();\n\tM getSQLResponse();\n\n\tMap<String, Object> getCustomMap();\n\tMap<String, Map<String, String>> getFunctionMap();\n\tMap<String, M> getChildMap();\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/OnParseCallback.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author Lemon\n */\npublic abstract class OnParseCallback<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\n\t/**\n\t * @param key\n\t * @param to\n\t * @param ro\n\t * @return false ? continue\n\t * @throws Exception\n\t */\n\tprotected boolean onParse(String key, Object to, Object ro) throws Exception {\n\t\treturn true;\n\t}\n\n\t/**\n\t * @param key\n\t * @param to\n\t * @param ro\n\t * @return\n\t * @throws Exception\n\t */\n\tprotected Object onParseObject(String key, Object to, Object ro) throws Exception {\n\t\treturn ro;\n\t}\n\n\t/**\n\t * @param key\n\t * @param tobj\n\t * @param robj\n\t * @return\n\t * @throws Exception\n\t */\n\tprotected M onParseJSONObject(String key, M tobj, M robj) throws Exception {\n\t\treturn robj;\n\t}\n\n\t/**\n\t * @param key\n\t * @param tarray\n\t * @param rarray\n\t * @return\n\t * @throws Exception\n\t */\n\tprotected L onParseJSONArray(String key, L tarray, L rarray) throws Exception {\n\t\treturn rarray;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Operation.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\n/**对请求 JSON 的操作\n * @author Lemon\n */\npublic enum Operation {\n\t/**\n\t * 必须传的字段，结构是\n\t * \"key0,key1,key2...\"\n\t */\n\tMUST,\n\n\t/**\n\t * 不允许传的字段，结构是\n\t * \"key0,key1,key2...\"\n\t */\n\tREFUSE,\n\t\n\t/**TODO 是否应该把数组类型写成 BOOLEANS, NUMBERS 等复数单词，以便抽取 enum ？扩展用 VERIFY 或 INSERT/UPDATE 远程函数等\n\t * 验证是否符合预设的类型:\n\t * BOOLEAN, NUMBER, DECIMAL, STRING, URL, DATE, TIME, DATETIME, OBJECT, ARRAY \n\t * 或它们的数组\n\t * BOOLEAN[], NUMBER[], DECIMAL[], STRING[], URL[], DATE[], TIME[], DATETIME[], OBJECT[], ARRAY[]\n\t * 结构是\n\t * {\n\t *   key0: value0,\n\t *   key1: value1,\n\t *   key2: value2\n\t *   ...\n\t * }\n\t * 例如\n\t * {\n\t *   \"id\": \"NUMBER\", //id 类型必须为 NUMBER\n\t *   \"pictureList\": \"URL[]\", //pictureList 类型必须为 URL[]\n\t * }\n\t * @see {@link AbstractVerifier#verifyType(String, String, Object, boolean)}\n\t */\n\tTYPE,\n\n\t/**\n\t * 验证是否符合预设的条件，结构是\n\t * {\n\t *   key0: value0,\n\t *   key1: value1,\n\t *   key2: value2\n\t *   ...\n\t * }\n\t * 例如\n\t * {\n\t *   \"phone~\": \"PHONE\",  //phone 必须满足 PHONE 的格式，配置见 {@link AbstractVerifier#COMPILE_MAP}\n\t *   \"status{}\": [1,2,3],  //status 必须在给出的范围内\n\t *   \"content[{}\": \">0\",  //content的长度 必须在给出的范围内\n\t *   \"pictureList{&{}\": \">0,<=10\",  //pictureList 的 JSON 长度必须在给出的范围内\n\t *   \"balance&{}\":\">0,<=10000\"  //必须满足 balance>0 & balance<=10000\n\t * }\n\t */\n\tVERIFY,\n\t\n\t/**\n\t * 验证是否存在，结构是\n\t * \"key0,key1,key2...\"\n\t * 多个字段用逗号隔开，联合校验\n\t */\n\tEXIST,\n\t\n\t/**\n\t * 验证是否不存在，除了本身的记录，结构是\n\t * \"key0,key1,key2...\"\n\t * 多个字段用逗号隔开，联合校验\n\t */\n\tUNIQUE,\n\t\n\t\n\t/**\n\t * 添加，当要被添加的对象不存在时，结构是\n\t * {\n\t *   key0: value0,\n\t *   key1: value1,\n\t *   key2: value2\n\t *   ...\n\t * }\n\t */\n\tINSERT,\n\t\n\t/**\n\t * 强行放入，不存在时就添加，存在时就修改，结构是\n\t * {\n\t *   key0: value0,\n\t *   key1: value1,\n\t *   key2: value2\n\t *   ...\n\t * }\n\t */\n\tUPDATE,\n\t\n\t/**\n\t * 替换，当要被替换的对象存在时，结构是\n\t * {\n\t *   key0: value0,\n\t *   key1: value1,\n\t *   key2: value2\n\t *   ...\n\t * }\n\t */\n\tREPLACE,\n\t\n\t/**\n\t * 移除，当要被移除的对象存在时，结构是\n\t * \"key0,key1,key2...\"\n\t */\n\tREMOVE,\n\n\t/**\n\t * 监听事件，用于同步到其它表，结构是\n\t * \"key0\": {}\n\t * 例如 \"name\": { \"UPDATE\": { \"Comment\": { \"userName@\": \"/name\" } } }\n\t * 当 User.name 被修改时，同步修改 Comment.userName\n\t *\n\t * 例如 \"sex != 0 && sex != 1\": \"throw new Error('sex 必须在 [0, 1] 内！')\"\n\t * 自定义代码，当满足条件是执行后面的代码\n\t *\n\t * 还有\n\t * \"ELSE\": \"\"\n\t * 自定义代码，不处理，和不传一样\n\t */\n\tIF,\n\n//\t/** 直接用 IF 替代\n//\t * 自定义代码，结构是 \"code\"，例如\n//\t * \"var a = 1;\n//\t *  var b = a + 2;\n//\t *  if (b % 2 == 0) {\n//\t *      throw new Error('b % 2 == 0 !');\n//\t *  }\n//\t * \"\n//\t *\n//\t * 或 { \"code\": \"JS\", \"code2\": \"LUA\" }\n//\t */\n//\tCODE,\n\n\t/**\n\t * 允许批量增删改部分失败，结构是\n\t * \"Table[],key[],key:alias[]\"\n\t * 自动 ALLOW_PARTIAL_UPDATE_FAILED_TABLE_MAP.put，结构是 Boolean，例如 true\n\t */\n\tALLOW_PARTIAL_UPDATE_FAIL,\n\n\t/**\n\t * 强制要求必须有 id/id{}/id{}@ 条件，结构是 Boolean，例如 true\n\t */\n\tIS_ID_CONDITION_MUST;\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Pair.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport apijson.StringUtil;\n\n/**key:value\n * @author Lemon\n */\npublic class Pair extends Entry<String, String> {\n\n\tprivate static final Map<String, Class<?>> CLASS_MAP;\n\tstatic {\n\t\tCLASS_MAP = new HashMap<String, Class<?>>();\n\t\tCLASS_MAP.put(boolean.class.getSimpleName(), boolean.class);\n\t\tCLASS_MAP.put(int.class.getSimpleName(), int.class);\n\t\tCLASS_MAP.put(long.class.getSimpleName(), long.class);\n\t\tCLASS_MAP.put(float.class.getSimpleName(), float.class);\n\t\tCLASS_MAP.put(double.class.getSimpleName(), double.class);\n\t\tCLASS_MAP.put(Boolean.class.getSimpleName(), Boolean.class);\n\t\tCLASS_MAP.put(Integer.class.getSimpleName(), Integer.class);\n\t\tCLASS_MAP.put(Long.class.getSimpleName(), Long.class);\n\t\tCLASS_MAP.put(Float.class.getSimpleName(), Float.class);\n\t\tCLASS_MAP.put(Double.class.getSimpleName(), Double.class);\n\n\t\tCLASS_MAP.put(Object.class.getSimpleName(), Object.class);\n\t\tCLASS_MAP.put(String.class.getSimpleName(), String.class);\n\t\tCLASS_MAP.put(Collection.class.getSimpleName(), Collection.class);//不允许指定<T>\n\t\tCLASS_MAP.put(Map.class.getSimpleName(), Map.class);//不允许指定<T>\n//\t\tCLASS_MAP.put(JSONMap.class.getSimpleName(), JSONMap.class);//必须有，Map中没有getLongValue等方法\n//\t\tCLASS_MAP.put(JSONList.class.getSimpleName(), JSONList.class);//必须有，Collection中没有getJSONObject等方法\n\t}\n\n\n\tpublic Pair() {\n\t\tsuper();\n\t}\n\n\tpublic boolean isEmpty(boolean trim) {\n\t\treturn StringUtil.isNotEmpty(key, trim) == false && StringUtil.isNotEmpty(value, trim) == false;\n\t}\n\n\t/**\n\t * @param <K>\n\t * @param pair\n\t * @return\n\t */\n\tpublic static <K, V> boolean isCorrect(Entry<K, V> pair) {\n\t\treturn pair != null && StringUtil.isNotEmpty(pair.getValue(), true);\n\t}\n\n\t/**\n\t * @return\n\t */\n\tpublic String toPairString() {\n\t\treturn toPairString(getKey(), getValue());\n\t}\n\t/**\n\t * @param typeKey\n\t * @param valueKey\n\t * @return\n\t */\n\tpublic static String toPairString(String typeKey, String valueKey) {\n\t\treturn (typeKey == null ? \"\" : typeKey + \":\") + valueKey;\n\t}\n\t/**\n\t * @param type\n\t * @param value\n\t * @return\n\t */\n\tpublic static String toPairString(Class<?> type, Object value) {\n\t\treturn toPairString(type == null ? null : type.getSimpleName(), StringUtil.get(value));\n\t}\n\n\t/**\n\t * isRightValueDefault = false;\n\t * \"key\":null不应该出现？因为FastJSON内默认不存null\n\t * @param pair leftKey:rightValue\n\t * @return {@link #parseEntry(String, boolean)}\n\t */\n\tpublic static Entry<String, String> parseEntry(String pair) {\n\t\treturn parseEntry(pair, false);\n\t}\n\t/**\n\t * isRightValueDefault = false;\n\t * \"key\":null不应该出现？因为FastJSON内默认不存null\n\t * @param pair leftKey:rightValue\n\t * @param isRightValueDefault 右边值缺省，当pair不包含 : 时默认整个pair为leftKey；false-相反\n\t * @return {@link #parseEntry(String, boolean, String)}\n\t */\n\tpublic static Entry<String, String> parseEntry(String pair, boolean isRightValueDefault) {\n\t\treturn parseEntry(pair, isRightValueDefault, null);\n\t}\n\t/**\n\t * \"key\":null不应该出现？因为FastJSON内默认不存null\n\t * @param pair leftKey:rightValue\n\t * @param isRightValueDefault 右边值缺省，当pair不包含 : 时默认整个pair为leftKey；false-相反\n\t * @param defaultValue 缺省值\n\t * @return @NonNull\n\t */\n\tpublic static Entry<String, String> parseEntry(String pair, boolean isRightValueDefault, String defaultValue) {\n\t\tpair = StringUtil.get(pair);//让客户端去掉所有空格 getNoBlankString(pair);\n\t\tEntry<String, String> entry = new Entry<String, String>();\n\t\tif (pair.isEmpty() == false) {\n\t\t\tint index = pair.indexOf(\":\");\n\t\t\tif (index < 0) {\n\t\t\t\tentry.setKey(isRightValueDefault ? pair : defaultValue);\n\t\t\t\tentry.setValue(isRightValueDefault ? defaultValue : pair);\n\t\t\t} else {\n\t\t\t\tentry.setKey(pair.substring(0, index));\n\t\t\t\tentry.setValue(pair.substring(index + 1, pair.length()));\n\t\t\t}\n\n\t\t}\n\t\treturn entry;\n\t}\n\t/**\n\t * @param pair\n\t * @return\n\t */\n\tpublic static Entry<String, String> parseVariableEntry(String pair) {\n\t\treturn parseEntry(pair, false, Object.class.getSimpleName());\n\t}\n\t/**\n\t * @param pair\n\t * @param valueMap\n\t * @return\n\t */\n\tpublic static Entry<Class<?>, Object> parseVariableEntry(String pair, Map<String, Object> valueMap) {\n\t\tpair = StringUtil.get(pair);//让客户端去掉所有空格 getNoBlankString(pair);\n\t\tEntry<Class<?>, Object> entry = new Entry<Class<?>, Object>();\n\t\tif (pair.isEmpty() == false) {\n\t\t\tint index = pair.contains(\":\") ? pair.indexOf(\":\") : -1;\n\n\t\t\tentry.setKey(CLASS_MAP.get(index < 0 ? Object.class.getSimpleName() : pair.substring(0, index)));\n\t\t\tentry.setValue(valueMap == null ? null : valueMap.get(pair.substring(index + 1, pair.length())));\n\t\t}\n\t\treturn entry;\n\t}\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Parser.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.sql.SQLException;\nimport java.sql.Savepoint;\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.*;\n\n/**解析器\n * @author Lemon\n */\npublic interface Parser<T, M extends Map<String, Object>, L extends List<Object>>\n\t\textends ParserCreator<T, M, L>, VerifierCreator<T, M, L>, SQLCreator<T, M, L> {\n\n\t@NotNull\n\tVisitor<T> getVisitor();\n\tParser<T, M, L> setVisitor(@NotNull Visitor<T> visitor);\n\n\t@NotNull\n\tRequestMethod getMethod();\n\tParser<T, M, L> setMethod(@NotNull RequestMethod method);\n\n\tint getVersion();\n\tParser<T, M, L> setVersion(int version);\n\t\n\tString getTag();\n\tParser<T, M, L> setTag(String tag);\n\n\tM getRequest();\n\tParser<T, M, L> setRequest(M request);\n\n\tParser<T, M, L> setNeedVerify(boolean needVerify);\n\t\n\tboolean isNeedVerifyLogin();\n\tParser<T, M, L> setNeedVerifyLogin(boolean needVerifyLogin);\n\n\tboolean isNeedVerifyRole();\n\tParser<T, M, L> setNeedVerifyRole(boolean needVerifyRole);\n\n\tboolean isNeedVerifyContent();\n\tParser<T, M, L> setNeedVerifyContent(boolean needVerifyContent);\n\n\t\n\tString parse(String request);\n\tString parse(M request);\n\n\tM parseResponse(String request);\n\tM parseResponse(M request);\n\n\t// 没必要性能还差 JSONRequest parseCorrectResponse(String table, JSONRequest response) throws Exception;\n\n\n\tM parseCorrectRequest() throws Exception;\n\t\n\tM parseCorrectRequest(RequestMethod method, String tag, int version, String name, M request,\n\t\t\tint maxUpdateCount, SQLCreator<T, M, L> creator) throws Exception;\n\n\n\tMap<String, Object> getStructure(String table, String method, String tag, int version) throws Exception;\n\n\n\tM onObjectParse(M request, String parentPath, String name, SQLConfig<T, M, L> arrayConfig, boolean isSubquery, M cache) throws Exception;\n\n\tL onArrayParse(M request, String parentPath, String name, boolean isSubquery, L cache) throws Exception;\n\n\t/**解析远程函数\n\t * @param key\n\t * @param function\n\t * @param parentPath\n\t * @param currentName\n\t * @param currentObject\n\t * @return\n\t * @throws Exception\n\t */\n\tObject onFunctionParse(String key, String function, String parentPath, String currentName, M currentObject, boolean containRaw) throws Exception;\n\t\n\tObjectParser<T, M, L> createObjectParser(M request, String parentPath, SQLConfig<T, M, L> arrayConfig, boolean isSubquery, boolean isTable, boolean isArrayMainTable) throws Exception;\n\n\tint getMinQueryPage();\n\tint getMaxQueryPage();\n\tint getDefaultQueryCount();\n\tint getMaxQueryCount();\n\tint getMaxUpdateCount();\n\tint getMaxSQLCount();\n\tint getMaxObjectCount();\n\tint getMaxArrayCount();\n\tint getMaxQueryDepth();\n\t\n\tvoid putQueryResult(String path, Object result);\n\n\n\tObject getValueByPath(String valuePath);\n\n\n\tvoid onVerifyLogin() throws Exception;\n\tvoid onVerifyContent() throws Exception;\n\tvoid onVerifyRole(SQLConfig<T, M, L> config) throws Exception;\n\t\n\tM executeSQL(SQLConfig<T, M, L> config, boolean isSubquery) throws Exception;\n\t\n\tSQLExecutor<T, M, L> getSQLExecutor();\n\tVerifier<T, M, L> getVerifier();\n\t\n\t\n\tBoolean getGlobalFormat();\n\tString getGlobalRole();\n\tString getGlobalDatabase();\n\tString getGlobalDatasource();\n\tString getGlobalNamespace();\n\tString getGlobalCatalog();\n\tString getGlobalSchema();\n\tBoolean getGlobalExplain();\n\tString getGlobalCache();\n\n\t\n\tint getTransactionIsolation();\n\tvoid setTransactionIsolation(int transactionIsolation);\n\t\n\tvoid begin(int transactionIsolation);\n\tvoid rollback() throws SQLException;\n\tvoid rollback(Savepoint savepoint) throws SQLException;\n\tvoid commit() throws SQLException;\n\tvoid close();\n\n\tM newSuccessResult();\n\tM newErrorResult(Exception e);\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/ParserCreator.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.NotNull;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**SQL相关创建器\n * @author Lemon\n */\npublic interface ParserCreator<T, M extends Map<String, Object>, L extends List<Object>> {\n\t\n\t@NotNull\n\tParser<T, M, L> createParser();\n\t\n\t@NotNull\n\tFunctionParser<T, M, L> createFunctionParser();\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/SQLConfig.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.NotNull;\nimport apijson.RequestMethod;\nimport apijson.StringUtil;\n\n/**SQL配置\n * @author Lemon\n */\npublic interface SQLConfig<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\tString DATABASE_MYSQL = \"MYSQL\"; // https://www.mysql.com\n\tString DATABASE_POSTGRESQL = \"POSTGRESQL\"; // https://www.postgresql.org\n\tString DATABASE_SQLSERVER = \"SQLSERVER\"; // https://www.microsoft.com/en-us/sql-server\n\tString DATABASE_ORACLE = \"ORACLE\"; // https://www.oracle.com/database\n\tString DATABASE_DB2 = \"DB2\"; // https://www.ibm.com/products/db2\n\tString DATABASE_MARIADB = \"MARIADB\"; // https://mariadb.org\n\tString DATABASE_TIDB = \"TIDB\"; // https://www.pingcap.com/tidb\n\tString DATABASE_COCKROACHDB = \"COCKROACHDB\"; // https://www.cockroachlabs.com\n\tString DATABASE_DAMENG = \"DAMENG\"; // https://www.dameng.com\n\tString DATABASE_KINGBASE = \"KINGBASE\"; // https://www.kingbase.com.cn\n\tString DATABASE_ELASTICSEARCH = \"ELASTICSEARCH\"; // https://www.elastic.co/guide/en/elasticsearch/reference/7.4/xpack-sql.html\n\tString DATABASE_MANTICORE = \"MANTICORE\"; // https://manticoresearch.com\n\tString DATABASE_CLICKHOUSE = \"CLICKHOUSE\"; // https://clickhouse.com\n\tString DATABASE_HIVE = \"HIVE\"; // https://hive.apache.org\n\tString DATABASE_PRESTO = \"PRESTO\"; // Facebook PrestoDB  https://prestodb.io\n\tString DATABASE_TRINO = \"TRINO\"; // PrestoSQL  https://trino.io\n\tString DATABASE_DORIS = \"DORIS\"; // https://doris.apache.org\n\tString DATABASE_SNOWFLAKE = \"SNOWFLAKE\"; // https://www.snowflake.com\n\tString DATABASE_DATABEND = \"DATABEND\"; // https://www.databend.com\n\tString DATABASE_DATABRICKS = \"DATABRICKS\"; // https://www.databricks.com\n\tString DATABASE_CASSANDRA = \"CASSANDRA\"; // https://cassandra.apache.org\n\tString DATABASE_MILVUS = \"MILVUS\"; // https://milvus.io\n\tString DATABASE_INFLUXDB = \"INFLUXDB\"; // https://www.influxdata.com/products/influxdb-overview\n\tString DATABASE_TDENGINE = \"TDENGINE\"; // https://tdengine.com\n\tString DATABASE_TIMESCALEDB = \"TIMESCALEDB\"; // https://www.timescale.com\n\tString DATABASE_QUESTDB = \"QUESTDB\"; // https://questdb.com\n\tString DATABASE_IOTDB = \"IOTDB\"; // https://iotdb.apache.org/zh/UserGuide/latest/API/Programming-JDBC.html\n\n\tString DATABASE_REDIS = \"REDIS\"; // https://redisql.com\n\tString DATABASE_MONGODB = \"MONGODB\"; // https://www.mongodb.com/docs/atlas/data-federation/query/query-with-sql\n\tString DATABASE_KAFKA = \"KAFKA\"; // https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Kafka\n\tString DATABASE_SQLITE = \"SQLITE\"; // https://www.sqlite.org\n\tString DATABASE_DUCKDB = \"DUCKDB\"; // https://duckdb.org\n\tString DATABASE_SURREALDB = \"SURREALDB\"; // https://surrealdb.com\n\tString DATABASE_OPENGAUSS = \"OPENGAUSS\"; // https://opengauss.org\n\n\tString DATABASE_MQ = \"MQ\"; //\n\n\tString SCHEMA_INFORMATION = \"information_schema\";  //MySQL, PostgreSQL, SQL Server 都有的系统模式\n\tString SCHEMA_SYS = \"sys\";  //SQL Server 系统模式\n\tString TABLE_SCHEMA = \"table_schema\";\n\tString TABLE_NAME = \"table_name\";\n\n\tint TYPE_CHILD = 0;\n\tint TYPE_ITEM = 1;\n\tint TYPE_ITEM_CHILD_0 = 2;\n\n\tParser<T, M, L> gainParser();\n\n\tSQLConfig<T, M, L> setParser(Parser<T, M, L> parser);\n\n\tObjectParser<T, M, L> gainObjectParser();\n\n\tSQLConfig<T, M, L> setObjectParser(ObjectParser<T, M, L> objectParser);\n\n\tint getVersion();\n\n\tSQLConfig<T, M, L> setVersion(int version);\n\n\tString getTag();\n\n\tSQLConfig<T, M, L> setTag(String tag);\n\n\tboolean isTSQL();\n\tboolean isMSQL();\n\tboolean isPSQL();\n\n\tboolean isMySQL();\n\tboolean isPostgreSQL();\n\tboolean isSQLServer();\n\tboolean isOracle();\n\tboolean isDb2();\n\tboolean isMariaDB();\n\tboolean isTiDB();\n\tboolean isCockroachDB();\n\tboolean isDameng();\n\tboolean isKingBase();\n\tboolean isElasticsearch();\n\tboolean isManticore();\n\tboolean isClickHouse();\n\tboolean isHive();\n\tboolean isPresto();\n\tboolean isTrino();\n\tboolean isSnowflake();\n\tboolean isDatabend();\n\tboolean isDatabricks();\n\tboolean isCassandra();\n\tboolean isMilvus();\n\tboolean isInfluxDB();\n\tboolean isTDengine();\n\tboolean isTimescaleDB();\n\tboolean isQuestDB();\n\tboolean isIoTDB();\n\tboolean isRedis();\n\tboolean isMongoDB();\n\tboolean isKafka();\n\tboolean isMQ();\n\tboolean isSQLite();\n\tboolean isDuckDB();\n\tboolean isSurrealDB();\n\tboolean isOpenGauss();\n\tboolean isDoris();\n\n\n\t// 暂时只兼容以上几种\n\t//\tboolean isSQL();\n\t//\tboolean isTSQL();\n\t//\tboolean isPLSQL();\n\t//\tboolean isAnsiSQL();\n\n\t/**用来给 Table, Column 等系统属性表来绕过 MAX_SQL_COUNT 等限制\n\t * @return\n\t */\n\tboolean limitSQLCount();\n\n\t/**是否开启 WITH AS 表达式来简化 SQL 和提升性能\n\t * @return\n\t */\n\tboolean isWithAsEnable();\n\t/**允许增删改部分失败\n\t * @return\n\t */\n\tboolean allowPartialUpdateFailed();\n\n\t@NotNull\n\tString getIdKey();\n\t@NotNull\n\tString getUserIdKey();\n\n\n\t/**获取数据库版本号，可通过判断版本号解决一些 JDBC 驱动连接数据库的兼容问题\n\t * MYSQL: 8.0, 5.7, 5.6 等； PostgreSQL: 11, 10, 9.6 等\n\t * @return\n\t */\n\tString gainDBVersion();\n\n\t@NotNull\n\tdefault int[] gainDBVersionNums() {\n\t\tString dbVersion = StringUtil.noBlank(gainDBVersion());\n\t\tif (dbVersion.isEmpty()) {\n\t\t\treturn new int[]{0};\n\t\t}\n\n\t\tint index = dbVersion.indexOf(\"-\");\n\t\tif (index > 0) {\n\t\t\tdbVersion = dbVersion.substring(0, index);\n\t\t}\n\n\t\tString[] ss = dbVersion.split(\"[.]\");\n\t\tint[] nums = new int[Math.max(1, ss.length)];\n\t\tfor (int i = 0; i < ss.length; i++) {\n\t\t\tnums[i] = Integer.valueOf(ss[i]);\n\t\t}\n\n\t\treturn nums;\n\t}\n\n\t/**获取数据库地址\n\t * @return\n\t */\n\tString gainDBUri();\n\n\t/**获取数据库账号\n\t * @return\n\t */\n\tString gainDBAccount();\n\n\t/**获取数据库密码\n\t * @return\n\t */\n\tString gainDBPassword();\n\n\t/**获取SQL语句\n\t * @return\n\t * @throws Exception\n\t */\n\tString gainSQL(boolean prepared) throws Exception;\n\n\n\n\tboolean isTest();\n\tSQLConfig<T, M, L> setTest(boolean test);\n\n\tint getType();\n\tSQLConfig<T, M, L> setType(int type);\n\n\tint getCount();\n\tSQLConfig<T, M, L> setCount(int count);\n\n\tint getPage();\n\tSQLConfig<T, M, L> setPage(int page);\n\n\tint getQuery();\n\tSQLConfig<T, M, L> setQuery(int query);\n\n\tBoolean getCompat();\n\tSQLConfig<T, M, L> setCompat(Boolean compat);\n\n\tint getPosition();\n\tSQLConfig<T, M, L> setPosition(int position);\n\n\tint getCache();\n\tSQLConfig<T, M, L> setCache(int cache);\n\n\tboolean isExplain();\n\tSQLConfig<T, M, L> setExplain(boolean explain);\n\n\n\tRequestMethod getMethod();\n\tSQLConfig<T, M, L> setMethod(RequestMethod method);\n\n\tObject getId();\n\tSQLConfig<T, M, L> setId(Object id);\n\n\tObject getIdIn();\n\tSQLConfig<T, M, L> setIdIn(Object idIn);\n\n\tObject getUserId();\n\tSQLConfig<T, M, L> setUserId(Object userId);\n\n\tObject getUserIdIn();\n\tSQLConfig<T, M, L> setUserIdIn(Object userIdIn);\n\n\tString getRole();\n\tSQLConfig<T, M, L> setRole(String role);\n\n\tpublic boolean isDistinct();\n\tpublic SQLConfig<T, M, L> setDistinct(boolean distinct);\n\n\tString getDatabase();\n\tSQLConfig<T, M, L> setDatabase(String database);\n\n\tString getSQLNamespace();\n\tString getNamespace();\n\tSQLConfig<T, M, L> setNamespace(String namespace);\n\n\tString gainSQLCatalog();\n\tString getCatalog();\n\tSQLConfig<T, M, L> setCatalog(String catalog);\n\n\tString gainSQLSchema();\n\tString getSchema();\n\tSQLConfig<T, M, L> setSchema(String schema);\n\n\tString getDatasource();\n\tSQLConfig<T, M, L> setDatasource(String datasource);\n\n\tString getQuote();\n\n\tList<String> getJson();\n\tSQLConfig<T, M, L> setJson(List<String> json);\n\n\t/**请求传进来的Table名\n\t * @return\n\t * @see {@link #gainSQLTable()}\n\t */\n\tString getTable();\n\n\tSQLConfig<T, M, L> setTable(String table);\n\n\t/**数据库里的真实Table名\n\t * 通过 {@link AbstractSQLConfig.TABLE_KEY_MAP} 映射\n\t * @return\n\t */\n\tString gainSQLTable();\n\n\tString gainTablePath();\n\n\tMap<String, String> getKeyMap();\n\tSQLConfig<T, M, L> setKeyMap(Map<String, String> keyMap);\n\n\tList<String> getRaw();\n\tSQLConfig<T, M, L> setRaw(List<String> raw);\n\n\tSubquery<T, M, L> getFrom();\n\tSQLConfig<T, M, L> setFrom(Subquery<T, M, L> from);\n\n\tList<String> getColumn();\n\tSQLConfig<T, M, L> setColumn(List<String> column);\n\n\tList<List<Object>> getValues();\n\tSQLConfig<T, M, L> setValues(List<List<Object>> values);\n\n\tMap<String, Object> getContent();\n\tSQLConfig<T, M, L> setContent(Map<String, Object> content);\n\n\tMap<String, List<String>> getCombineMap();\n\tSQLConfig<T, M, L> setCombineMap(Map<String, List<String>> combineMap);\n\n\tString getCombine();\n\tSQLConfig<T, M, L> setCombine(String combine);\n\n\tMap<String, String> getCast();\n\tSQLConfig<T, M, L> setCast(Map<String, String> cast);\n\n\tList<String> getNull();\n\tSQLConfig<T, M, L> setNull(List<String> nulls);\n\n\tMap<String, Object> getWhere();\n\tSQLConfig<T, M, L> setWhere(Map<String, Object> where);\n\n\tString getGroup();\n\tSQLConfig<T, M, L> setGroup(String group);\n\n\tMap<String, Object> getHaving();\n\tSQLConfig<T, M, L> setHaving(Map<String, Object> having);\n\n\tString getHavingCombine();\n\tSQLConfig<T, M, L> setHavingCombine(String havingCombine);\n\n\tString getSample();\n\tSQLConfig<T, M, L> setSample(String order);\n\n\tString getLatest();\n\tSQLConfig<T, M, L> setLatest(String latest);\n\n\tString getPartition();\n\tSQLConfig<T, M, L> setPartition(String partition);\n\n\tString getFill();\n\tSQLConfig<T, M, L> setFill(String fill);\n\n\tString getOrder();\n\tSQLConfig<T, M, L> setOrder(String order);\n\n\t/**\n\t * exactMatch = false\n\t * @param key\n\t * @return\n\t */\n\tObject getWhere(String key);\n\t/**\n\t * @param key\n\t * @param exactMatch\n\t * @return\n\t */\n\tObject getWhere(String key, boolean exactMatch);\n\t/**\n\t * @param key\n\t * @param value\n\t * @return\n\t */\n\tSQLConfig<T, M, L> putWhere(String key, Object value, boolean prior);\n\n\n\tboolean isPrepared();\n\n\tSQLConfig<T, M, L> setPrepared(boolean prepared);\n\n\tboolean isMain();\n\n\tSQLConfig<T, M, L> setMain(boolean main);\n\n\n\tList<Object> getPreparedValueList();\n\tSQLConfig<T, M, L> setPreparedValueList(List<Object> preparedValueList);\n\n\n\tString getAlias();\n\n\tSQLConfig<T, M, L> setAlias(String alias);\n\n\tdefault String gainTableKey() {\n\t\tString alias = getAlias();\n\t\treturn getTable() + (StringUtil.isEmpty(alias) ? \"\" : \":\" + alias);\n\t}\n\n\tdefault String gainSQLAlias() {\n\t\treturn gainSQLAlias(getTable(), getAlias());\n\t}\n\tstatic String gainSQLAlias(@NotNull String table, String alias) {\n\t\t// 这里不用 : $ 等符号，因为部分数据库/引擎似乎不支持 `key`, \"key\", [key] 等避免关键词冲突的方式，只能使用符合变量命名的表别名\n\t\treturn StringUtil.isEmpty(alias) ? table : table + \"__\" + alias; // 带上原表名，避免 alias 和其它表名/字段名冲突\n\t}\n\n\n\tString gainWhereString(boolean hasPrefix) throws Exception;\n\n\tString gainRawSQL(String key, Object value) throws Exception;\n\tString gainRawSQL(String key, Object value, boolean throwWhenMissing) throws Exception;\n\n\tboolean isKeyPrefix();\n\n\tSQLConfig<T, M, L> setKeyPrefix(boolean keyPrefix);\n\n\tList<Join<T, M, L>> getJoinList();\n\tSQLConfig<T, M, L> setJoinList(List<Join<T, M, L>> joinList);\n\n\tboolean hasJoin();\n\n\n\tString gainSubqueryString(Subquery<T, M, L> subquery) throws Exception;\n\n\tSQLConfig<T, M, L> setProcedure(String procedure);\n\n\n\tList<Object> getWithAsExprPreparedValueList();\n\tSQLConfig<T, M, L> setWithAsExprPreparedValueList(List<Object> withAsExprePreparedValueList);\n\n\tboolean isFakeDelete();\n\n\tMap<String, Object> onFakeDelete(Map<String, Object> map);\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/SQLCreator.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.NotNull;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**SQL相关创建器\n * @author Lemon\n */\npublic interface SQLCreator<T, M extends Map<String, Object>, L extends List<Object>> {\n\t\n\t@NotNull\n\tSQLConfig<T, M, L> createSQLConfig();\n\n\t@NotNull\n\tSQLExecutor<T, M, L> createSQLExecutor();\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/SQLExecutor.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\r\n\r\nThis source code is licensed under the Apache License Version 2.0.*/\r\n\r\n\r\npackage apijson.orm;\r\n\r\nimport java.sql.Connection;\r\nimport java.sql.ResultSet;\r\nimport java.sql.ResultSetMetaData;\r\nimport java.sql.SQLException;\r\nimport java.sql.Savepoint;\r\nimport java.sql.Statement;\r\nimport java.util.List;\r\nimport java.util.Map;\r\n\r\nimport apijson.*;\r\n\r\n/**executor for query(read) or update(write) MySQL database\r\n * @author Lemon\r\n */\r\npublic interface SQLExecutor<T, M extends Map<String, Object>, L extends List<Object>> {\r\n\tParser<T, M, L> getParser();\r\n\tSQLExecutor<T, M, L> setParser(Parser<T, M, L> parser);\r\n\r\n\t/**保存缓存\r\n\t * @param sql\r\n\t * @param list\r\n\t * @param config\r\n\t */\r\n\tvoid putCache(String sql, List<M> list, SQLConfig<T, M, L> config);\r\n\r\n\t/**获取缓存\r\n\t * @param sql\r\n\t * @param config\r\n\t * @return\r\n\t */\r\n\tList<M> getCache(String sql, SQLConfig<T, M, L> config);\r\n\r\n\t/**获取缓存\r\n\t * @param sql\r\n\t * @param position\r\n\t * @param config\r\n\t * @return\r\n\t */\r\n\tM getCacheItem(String sql, int position, SQLConfig<T, M, L> config);\r\n\r\n\t/**移除缓存\r\n\t * @param sql\r\n\t * @param config\r\n\t */\r\n\tvoid removeCache(String sql, SQLConfig<T, M, L> config);\r\n\r\n\t/**执行SQL\r\n\t * @param config\r\n\t * @param unknownType\r\n\t * @return\r\n\t * @throws Exception\r\n\t */\r\n\tM execute(@NotNull SQLConfig<T, M, L> config, boolean unknownType) throws Exception;\r\n\r\n\t//executeQuery和executeUpdate这两个函数因为返回类型不同，所以不好合并\r\n\t/**执行查询\r\n\t * @param config\r\n\t * @return\r\n\t * @throws SQLException\r\n\t */\r\n\tdefault ResultSet executeQuery(@NotNull SQLConfig<T, M, L> config) throws Exception {\r\n\t\treturn executeQuery(config, null);\r\n\t}\r\n\tResultSet executeQuery(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception;\r\n\r\n\t/**执行增、删、改\r\n\t * @param config\r\n\t * @return\r\n\t * @throws SQLException\r\n\t */\r\n\tdefault int executeUpdate(@NotNull SQLConfig<T, M, L> config) throws Exception {\r\n\t\treturn executeUpdate(config, null);\r\n\t}\r\n\tint executeUpdate(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception;\r\n\r\n\r\n\t/**判断是否为JSON类型\r\n\t* @param config\r\n\t* @param rsmd\r\n\t* @param position\r\n\t* @param label\r\n\t* @return\r\n\t*/\r\n\tboolean isJSONType(@NotNull SQLConfig<T, M, L> config, ResultSetMetaData rsmd, int position, String label);\r\n\r\n\r\n\tConnection getConnection(@NotNull SQLConfig<T, M, L> config) throws Exception;\r\n\tdefault Statement getStatement(@NotNull SQLConfig<T, M, L> config) throws Exception {\r\n\t\treturn getStatement(config, null);\r\n\t}\r\n\tStatement getStatement(@NotNull SQLConfig<T, M, L> config, String sql) throws Exception;\r\n\r\n\tint getTransactionIsolation();\r\n\tvoid setTransactionIsolation(int transactionIsolation);\r\n\t/**开始事务\r\n\t * @throws SQLException\r\n\t */\r\n\tvoid begin(int transactionIsolation) throws SQLException;\r\n\t/**回滚事务\r\n\t * @throws SQLException\r\n\t */\r\n\tvoid rollback() throws SQLException;\r\n\t/**提交事务\r\n\t * @throws SQLException\r\n\t */\r\n\tvoid rollback(Savepoint savepoint) throws SQLException;\r\n\t/**提交事务\r\n\t * @throws SQLException\r\n\t */\r\n\tvoid commit() throws SQLException;\r\n\t/**关闭连接，释放资源\r\n\t */\r\n\tvoid close();\r\n\r\n\tResultSet executeQuery(@NotNull Statement statement, String sql) throws Exception;\r\n\r\n\tint executeUpdate(@NotNull Statement statement, String sql) throws Exception;\r\n\r\n\tResultSet execute(@NotNull Statement statement, String sql) throws Exception;\r\n\r\n\tint getGeneratedSQLCount();\r\n\r\n\tint getCachedSQLCount();\r\n\r\n\tint getExecutedSQLCount();\r\n\r\n\tlong getExecutedSQLDuration();\r\n\r\n\tlong getSqlResultDuration();\r\n\r\n}\r\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Subquery.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**子查询 配置\n * @author Lemon\n */\npublic class Subquery<T, M extends Map<String, Object>, L extends List<Object>> {\n\t\n\tprivate String path; // []/0/User\n\tprivate String originKey; //id{}@\n\tprivate M originValue; // { \"from\": \"Comment\", \"Comment\": {...} }\n\n\tprivate String from; // Comment\n\tprivate String range; // ANY, ALL\n\tprivate String key; //id{}\n\tprivate SQLConfig<T, M, L> config;\n\t\n\tpublic String gainPath() {\n\t\treturn path;\n\t}\n\tpublic void setPath(String path) {\n\t\tthis.path = path;\n\t}\n\t\n\tpublic String gainOriginKey() {\n\t\treturn originKey;\n\t}\n\tpublic void setOriginKey(String originKey) {\n\t\tthis.originKey = originKey;\n\t}\n\t\n\tpublic M gainOriginValue() {\n\t\treturn originValue;\n\t}\n\tpublic void setOriginValue(M originValue) {\n\t\tthis.originValue = originValue;\n\t}\n\t\n\tpublic String gainFrom() {\n\t\treturn from;\n\t}\n\tpublic void setFrom(String from) {\n\t\tthis.from = from;\n\t}\n\t\n\tpublic String gainRange() {\n\t\treturn range;\n\t}\n\tpublic void setRange(String range) {\n\t\tthis.range = range;\n\t}\n\t\n\tpublic String gainKey() {\n\t\treturn key;\n\t}\n\tpublic void setKey(String key) {\n\t\tthis.key = key;\n\t}\n\t\n\tpublic SQLConfig<T, M, L> gainConfig() {\n\t\treturn config;\n\t}\n\tpublic void setConfig(SQLConfig<T, M, L> config) {\n\t\tthis.config = config;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Verifier.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.*;\n\n/**校验器(权限、请求参数、返回结果等)\n * @author Lemon\n */\npublic interface Verifier<T, M extends Map<String, Object>, L extends List<Object>> {\n\n\n\t/**验证权限是否通过\n\t * @param config\n\t * @return\n\t * @throws Exception\n\t */\n\tboolean verifyAccess(SQLConfig<T, M, L> config) throws Exception;\n\n\n\t/**校验请求使用的角色，角色不好判断，让访问者发过来角色名，OWNER,CONTACT,ADMIN等\n\t * @param config\n\t * @param table\n\t * @param method\n\t * @param role\n\t * @return\n\t * @throws Exception \n\t * @see {@link JSONMap#KEY_ROLE}\n\t */\n\tvoid verifyRole(SQLConfig<T, M, L> config, String table, RequestMethod method, String role) throws Exception;\n\n\t/**登录校验\n\t * @throws Exception\n\t */\n\tvoid verifyLogin() throws Exception;\n\t/**管理员角色校验\n\t * @throws Exception\n\t */\n\tvoid verifyAdmin() throws Exception;\n\n\n\n\t/**验证是否重复\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @throws Exception\n\t */\n\tvoid verifyRepeat(String table, String key, Object value) throws Exception;\n\t\n\t/**验证是否重复\n\t * @param table\n\t * @param key\n\t * @param value\n\t * @param exceptId 不包含id\n\t * @throws Exception\n\t */\n\tvoid verifyRepeat(String table, String key, Object value, long exceptId) throws Exception;\n\t\n\t/**验证请求参数的数据和结构\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param request\n\t * @param maxUpdateCount\n\t * @param globalDatabase\n\t * @param globalSchema\n\t * @param creator\n\t * @return\n\t * @throws Exception\n\t */\n\tM verifyRequest(RequestMethod method, String name, M target, M request,\n\t\t\tint maxUpdateCount, String globalDatabase, String globalSchema) throws Exception;\n\n\t/**验证返回结果的数据和结构\n\t * @param method\n\t * @param name\n\t * @param target\n\t * @param response\n\t * @param database\n\t * @param schema\n\t * @param creator\n\t * @param callback\n\t * @return\n\t * @throws Exception\n\t */\n\tM verifyResponse(\n\t\tRequestMethod method, String name, M target, M response,\n\t\tString database, String schema, @NotNull Parser<T, M, L> parser, OnParseCallback<T, M, L> callback\n\t) throws Exception;\n\n\n\t@NotNull\n\tParser<T, M, L> createParser();\n\n\tParser<T, M, L> getParser();\n\tVerifier<T, M, L> setParser(AbstractParser<T, M, L> parser);\n\n\t@NotNull\n\tVisitor<T> getVisitor();\n\tVerifier<T, M, L> setVisitor(@NotNull Visitor<T> visitor);\n\t\n\tString getVisitorIdKey(SQLConfig<T, M, L> config);\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/VerifierCreator.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport apijson.NotNull;\n\nimport java.util.List;\nimport java.util.Map;\n\n/**验证器相关创建器\n * @author Lemon\n */\npublic interface VerifierCreator<T, M extends Map<String, Object>, L extends List<Object>> {\n\t\n\t@NotNull\n\tVerifier<T, M, L> createVerifier();\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/Visitor.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm;\n\nimport java.util.List;\n\n/**来访者\n * @author Lemon\n */\npublic interface Visitor<T> {\n\n\tT getId();\n\n\tList<T> getContactIdList();\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/CommonException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.HttpRetryException;\nimport java.sql.SQLException;\nimport java.util.concurrent.TimeoutException;\n\nimport apijson.JSONResponse;\nimport apijson.Log;\nimport apijson.StringUtil;\nimport apijson.orm.AbstractSQLConfig;\nimport apijson.orm.SQLConfig;\n\n/**异常包装器，主要用来包装构造器没有 Throwable 参数的 Exception\n * @author Lemon\n */\npublic class CommonException extends Exception {\n  private static final long serialVersionUID = 1L;\n  private Integer code;\n  private String environment;\n\n  public CommonException setCode(Integer code) {\n    this.code = code;\n    return this;\n  }\n  public Integer getCode() {\n    if (code == null) {\n      code = getCode(getCause());\n    }\n    return code;\n  }\n\n\n  public static int getCode(Throwable e) {\n    boolean isCommon = Log.DEBUG && e instanceof CommonException;\n\n    Integer code = isCommon ? ((CommonException) e).getCode() : null;\n    if (code != null) {\n      return code;\n    }\n\n    Throwable t = isCommon ? e.getCause() : e;\n    if (t == null) {\n      return JSONResponse.CODE_SERVER_ERROR;\n    }\n\n    if (t instanceof HttpRetryException) {\n      return ((HttpRetryException) t).responseCode();\n    }\n\n    if (t instanceof UnsupportedEncodingException) {\n      return JSONResponse.CODE_UNSUPPORTED_ENCODING;\n    }\n\n    if (t instanceof IllegalAccessException) {\n      return JSONResponse.CODE_ILLEGAL_ACCESS;\n    }\n\n    if (t instanceof UnsupportedOperationException) {\n      return JSONResponse.CODE_UNSUPPORTED_OPERATION;\n    }\n\n    if (t instanceof NotExistException) {\n      return JSONResponse.CODE_NOT_FOUND;\n    }\n\n    if (t instanceof IllegalArgumentException) {\n      return JSONResponse.CODE_ILLEGAL_ARGUMENT;\n    }\n\n    if (t instanceof NotLoggedInException) {\n      return JSONResponse.CODE_NOT_LOGGED_IN;\n    }\n\n    if (t instanceof TimeoutException) {\n      return JSONResponse.CODE_TIME_OUT;\n    }\n\n    if (t instanceof ConflictException) {\n      return JSONResponse.CODE_CONFLICT;\n    }\n\n    if (t instanceof ConditionErrorException) {\n      return JSONResponse.CODE_CONDITION_ERROR;\n    }\n\n    if (t instanceof UnsupportedDataTypeException) {\n      return JSONResponse.CODE_UNSUPPORTED_TYPE;\n    }\n\n    if (t instanceof OutOfRangeException) {\n      return JSONResponse.CODE_OUT_OF_RANGE;\n    }\n\n    if (t instanceof NullPointerException) {\n      return JSONResponse.CODE_NULL_POINTER;\n    }\n\n    return JSONResponse.CODE_SERVER_ERROR;\n  }\n\n  public static String getMsg(Throwable e) {\n    if (e == null) {\n      return null;\n    }\n\n    String msg = e.getMessage();\n    if (msg != null) {\n      return msg;\n    }\n\n    Throwable t = e.getCause();\n    return t == null ? null : t.getMessage();\n  }\n\n  public CommonException setEnvironment(String environment) {\n    this.environment = environment;\n    return this;\n  }\n\n  public String getEnvironment() {\n    return environment;\n  }\n\n  public CommonException(Throwable t) {\n    this(null, t);\n  }\n  public CommonException(String msg, Throwable t) {\n    super(msg == null && t != null ? t.getMessage() : null, t);\n  }\n\n  public CommonException(int code, String msg) {\n    this(code, msg, null);\n  }\n  public CommonException(int code, String msg, Throwable t) {\n    this(msg, t);\n    setCode(code);\n  }\n  public CommonException(Throwable t, String environment) {\n    this(null, t);\n    setEnvironment(environment);\n  }\n\n\n  public static Exception wrap(Exception e, SQLConfig<?, ?, ?> config) {\n    if (Log.DEBUG == false && e instanceof SQLException) {\n      return new SQLException(\"数据库驱动执行异常SQLException，非 Log.DEBUG 模式下不显示详情，避免泄漏真实模式名、表名等隐私信息\", e);\n    }\n\n    //\t\t\tString msg = e.getMessage();\n\n    if (Log.DEBUG && (e instanceof CommonException == false || ((CommonException) e).getEnvironment() == null)) {\n      // msg != null && msg.contains(Log.KEY_SYSTEM_INFO_DIVIDER) == false) {\n      try {\n        String db = config == null ? AbstractSQLConfig.DEFAULT_DATABASE : (config instanceof AbstractSQLConfig\n          ? ((AbstractSQLConfig<?, ?, ?>) config).gainSQLDatabase() : config.getDatabase()\n        );\n\n        String dbVersion = config == null ? null : config.gainDBVersion();\n        if (StringUtil.isEmpty(dbVersion)) {\n          dbVersion = \"<!-- 请填写版本号，例如 8.0 -->\";\n        }\n\n        if (db != null || config == null) {\n          db += \" \" + dbVersion;\n        }\n        else if (config.isMySQL()) {\n          db = SQLConfig.DATABASE_MYSQL + \" \" + dbVersion;\n        }\n        else if (config.isMariaDB()) {\n          db = SQLConfig.DATABASE_MARIADB + \" \" + dbVersion;\n        }\n        else if (config.isTiDB()) {\n          db = SQLConfig.DATABASE_TIDB + \" \" + dbVersion;\n        }\n        else if (config.isPostgreSQL()) {\n          db = SQLConfig.DATABASE_POSTGRESQL + \" \" + dbVersion;\n        }\n        else if (config.isCockroachDB()) {\n          db = SQLConfig.DATABASE_COCKROACHDB + \" \" + dbVersion;\n        }\n        else if (config.isSQLServer()) {\n          db = SQLConfig.DATABASE_SQLSERVER + \" \" + dbVersion;\n        }\n        else if (config.isOracle()) {\n          db = SQLConfig.DATABASE_ORACLE + \" \" + dbVersion;\n        }\n        else if (config.isDb2()) {\n          db = SQLConfig.DATABASE_DB2 + \" \" + dbVersion;\n        }\n        else if (config.isDuckDB()) {\n          db = SQLConfig.DATABASE_DUCKDB + \" \" + dbVersion;\n        }\n        else if (config.isSurrealDB()) {\n          db = SQLConfig.DATABASE_SURREALDB + \" \" + dbVersion;\n        }\n        else if (config.isOpenGauss()) {\n          db = SQLConfig.DATABASE_OPENGAUSS + \" \" + dbVersion;\n        }\n        else if (config.isDameng()) {\n          db = SQLConfig.DATABASE_DAMENG + \" \" + dbVersion;\n        }\n        else if (config.isKingBase()) {\n          db = SQLConfig.DATABASE_KINGBASE + \" \" + dbVersion;\n        }\n        else if (config.isElasticsearch()) {\n          db = SQLConfig.DATABASE_ELASTICSEARCH + \" \" + dbVersion;\n        }\n        else if (config.isManticore()) {\n          db = SQLConfig.DATABASE_MANTICORE + \" \" + dbVersion;\n        }\n        else if (config.isClickHouse()) {\n          db = SQLConfig.DATABASE_CLICKHOUSE + \" \" + dbVersion;\n        }\n        else if (config.isMilvus()) {\n          db = SQLConfig.DATABASE_MILVUS + \" \" + dbVersion;\n        }\n        else if (config.isInfluxDB()) {\n          db = SQLConfig.DATABASE_INFLUXDB + \" \" + dbVersion;\n        }\n        else if (config.isTDengine()) {\n          db = SQLConfig.DATABASE_TDENGINE + \" \" + dbVersion;\n        }\n        else if (config.isTimescaleDB()) {\n          db = SQLConfig.DATABASE_TIMESCALEDB + \" \" + dbVersion;\n        }\n        else if (config.isQuestDB()) {\n          db = SQLConfig.DATABASE_QUESTDB + \" \" + dbVersion;\n        }\n        else if (config.isIoTDB()) {\n          db = SQLConfig.DATABASE_IOTDB + \" \" + dbVersion;\n        }\n        else if (config.isSQLite()) {\n          db = SQLConfig.DATABASE_SQLITE + \" \" + dbVersion;\n        }\n        else if (config.isHive()) {\n          db = SQLConfig.DATABASE_HIVE + \" \" + dbVersion;\n        }\n        else if (config.isPresto()) {\n          db = SQLConfig.DATABASE_PRESTO + \" \" + dbVersion;\n        }\n        else if (config.isTrino()) {\n          db = SQLConfig.DATABASE_TRINO + \" \" + dbVersion;\n        }\n        else if (config.isDoris()) {\n          db = SQLConfig.DATABASE_DORIS + \" \" + dbVersion;\n        }\n        else if (config.isSnowflake()) {\n          db = SQLConfig.DATABASE_SNOWFLAKE + \" \" + dbVersion;\n        }\n        else if (config.isDatabend()) {\n          db = SQLConfig.DATABASE_DATABEND + \" \" + dbVersion;\n        }\n        else if (config.isDatabricks()) {\n          db = SQLConfig.DATABASE_DATABRICKS + \" \" + dbVersion;\n        }\n        else if (config.isMongoDB()) {\n          db = SQLConfig.DATABASE_MONGODB + \" \" + dbVersion;\n        }\n        else if (config.isCassandra()) {\n          db = SQLConfig.DATABASE_CASSANDRA + \" \" + dbVersion;\n        }\n        else if (config.isRedis()) {\n          db = SQLConfig.DATABASE_REDIS + \" \" + dbVersion;\n        }\n        else if (config.isKafka()) {\n          db = SQLConfig.DATABASE_KAFKA + \" \" + dbVersion;\n        }\n        else {\n          db = \"<!-- 请填写，例如 MySQL 5.7。获取到的默认数据库为 \" + AbstractSQLConfig.DEFAULT_DATABASE + \" -->\";\n        }\n\n//\t\t\t\t\tClass<? extends Exception> clazz = e.getClass();\n//          msg = msg\n//                      + \"       \" + Log.KEY_SYSTEM_INFO_DIVIDER + \"       **环境信息** \"\n        String env = \" **环境信息** \"\n          + \" \\n 系统: \" + Log.OS_NAME + \" \" + Log.OS_VERSION\n          + \" \\n 数据库: \" + db\n          + \" \\n JDK: \" + Log.JAVA_VERSION + \" \" + Log.OS_ARCH\n          + \" \\n APIJSON: \" + Log.VERSION;\n\n        if (e instanceof CommonException) {\n          ((CommonException) e).setEnvironment(env);\n          return e;\n        }\n\n//          try {\n//            e = clazz.getConstructor(String.class, Throwable.class).newInstance(msg, e);\n//          }\n//          catch (Throwable e2) {\n        return new CommonException(e, env);  // e = clazz.getConstructor(String.class).newInstance(msg);\n//          }\n      } catch (Throwable e2) {}\n    }\n\n    return e;\n  }\n\n}\n\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/ConditionErrorException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\n/**条件错误\n * @author Lemon\n */\npublic class ConditionErrorException extends Exception {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tpublic ConditionErrorException(String msg) {\n\t\tsuper(msg);\n\t}\n\tpublic ConditionErrorException(Throwable t) {\n\t\tsuper(t);\n\t}\n\tpublic ConditionErrorException(String msg, Throwable t) {\n\t\tsuper(msg, t);\n\t}\n\t\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/ConflictException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\n/**冲突\n * @author Lemon\n */\npublic class ConflictException extends Exception {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tpublic ConflictException(String msg) {\n\t\tsuper(msg);\n\t}\n\tpublic ConflictException(Throwable t) {\n\t\tsuper(t);\n\t}\n\tpublic ConflictException(String msg, Throwable t) {\n\t\tsuper(msg, t);\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/NotExistException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\n/**不存在，可接受，内部吃掉\n * @author Lemon\n */\npublic class NotExistException extends Exception {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tpublic NotExistException(String msg) {\n\t\tsuper(msg);\n\t}\n\tpublic NotExistException(Throwable t) {\n\t\tsuper(t);\n\t}\n\tpublic NotExistException(String msg, Throwable t) {\n\t\tsuper(msg, t);\n\t}\n\n}\n\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/NotLoggedInException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\n/**未登录\n * @author Lemon\n */\npublic class NotLoggedInException extends Exception {\n\tprivate static final long serialVersionUID = 1L;\n\n\tpublic NotLoggedInException(String msg, Throwable t) {\n\t\tsuper(msg, t);\n\t}\n\tpublic NotLoggedInException(String msg) {\n\t\tsuper(msg);\n\t}\n\tpublic NotLoggedInException(Throwable t) {\n\t\tsuper(t);\n\t}\n\t\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/OutOfRangeException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\n/**超出范围\n * @author Lemon\n */\npublic class OutOfRangeException extends Exception {\n\tprivate static final long serialVersionUID = 1L;\n\t\n\tpublic OutOfRangeException(String msg) {\n\t\tsuper(msg);\n\t}\n\tpublic OutOfRangeException(Throwable t) {\n\t\tsuper(t);\n\t}\n\tpublic OutOfRangeException(String msg, Throwable t) {\n\t\tsuper(msg, t);\n\t}\n\t\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/UnsupportedDataTypeException.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.exception;\n\nimport java.io.IOException;\n\n/**\n * 给定的数据类型不被支持\n *\n * @author cnscoo\n */\n\npublic class UnsupportedDataTypeException extends IOException {\n    private static final long serialVersionUID = 1L;\n\n    public UnsupportedDataTypeException() {\n        super();\n    }\n\n    public UnsupportedDataTypeException(String s) {\n        super(s);\n    }\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/exception/package-info.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\n/**\n * exception files package\n */\n/**\n * @author Lemon\n *\n */\npackage apijson.orm.exception;"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Access.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**访问权限\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Access {\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/AllColumn.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 在 sys 下的字段(列名)\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class AllColumn {\n\tpublic static final String TAG = \"AllColumn\";\n\tpublic static final String TABLE_NAME = \"ALL_TAB_COLUMNS\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/AllColumnComment.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 在 sys 下的字段(列名)\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class AllColumnComment {\n\tpublic static final String TAG = \"AllColumnComment\";\n\tpublic static final String TABLE_NAME = \"ALL_COL_COMMENTS\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/AllTable.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 表属性\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class AllTable {\n\tpublic static final String TAG = \"AllTable\";\n\tpublic static final String TABLE_NAME = \"ALL_TABLES\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/AllTableComment.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 表属性\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class AllTableComment {\n\tpublic static final String TAG = \"AllTableComment\";\n\tpublic static final String TABLE_NAME = \"ALL_TAB_COMMENTS\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Column.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**字段(列名)属性\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Column {\n\tpublic static final String TAG = \"Column\";\n\tpublic static final String TABLE_NAME = \"columns\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Document.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport static apijson.orm.AbstractVerifier.ADMIN;\nimport static apijson.orm.AbstractVerifier.LOGIN;\n\nimport java.io.Serializable;\nimport java.sql.Timestamp;\n\nimport apijson.MethodAccess;\n\n/**测试用例文档\n * @author Lemon\n */\n@MethodAccess(\n\t\tGET = { LOGIN, ADMIN }, \n\t\tHEAD = { LOGIN, ADMIN },\n\t\tPUT = { LOGIN, ADMIN }\n\t\t)\npublic class Document implements Serializable {\n\tprivate static final long serialVersionUID = 1L;\n\n\tprivate Long id; //唯一标识\n\tprivate Long userId; //用户id  应该用adminId，只有当登录账户是管理员时才能操作文档。  需要先建Admin表，新增登录等相关接口。\n\tprivate Integer version; //接口版本号  <=0 - 不限制版本，任意版本都可用这个接口  >0 - 在这个版本添加的接口\n\tprivate String name; //接口名称\n\tprivate String url; //请求地址\n\tprivate String request; //请求  用json格式会导致强制排序，而请求中引用赋值只能引用上面的字段，必须有序。\n\tprivate Timestamp date; //创建日期\n\tprivate String response; //标准返回结果\n\n\n\tpublic Document() {\n\t\tsuper();\n\t}\n\tpublic Document(long id) {\n\t\tthis();\n\t\tsetId(id);\n\t}\n\n\n\n\n\tpublic Long getId() {\n\t\treturn id;\n\t}\n\n\tpublic Document setId(Long id) {\n\t\tthis.id = id;\n\t\treturn this;\n\t}\n\n\tpublic Long getUserId() {\n\t\treturn userId;\n\t}\n\n\tpublic Document setUserId(Long userId) {\n\t\tthis.userId = userId;\n\t\treturn this;\n\t}\n\n\tpublic Integer getVersion() {\n\t\treturn version;\n\t}\n\n\tpublic Document setVersion(Integer version) {\n\t\tthis.version = version;\n\t\treturn this;\n\t}\n\n\tpublic String getName() {\n\t\treturn name;\n\t}\n\n\tpublic Document setName(String name) {\n\t\tthis.name = name;\n\t\treturn this;\n\t}\n\n\tpublic String getUrl() {\n\t\treturn url;\n\t}\n\n\tpublic Document setUrl(String url) {\n\t\tthis.url = url;\n\t\treturn this;\n\t}\n\n\tpublic String getRequest() {\n\t\treturn request;\n\t}\n\n\tpublic Document setRequest(String request) {\n\t\tthis.request = request;\n\t\treturn this;\n\t}\n\n\tpublic Timestamp getDate() {\n\t\treturn date;\n\t}\n\n\tpublic Document setDate(Timestamp date) {\n\t\tthis.date = date;\n\t\treturn this;\n\t}\n\n\tpublic String getResponse() {\n\t\treturn response;\n\t}\n\n\tpublic Document setResponse(String response) {\n\t\tthis.response = response;\n\t\treturn this;\n\t}\n\n\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/ExtendedProperty.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**扩展属性，SQL Server 转用\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class ExtendedProperty {\n\tpublic static final String TAG = \"ExtendedProperty\";\n\tpublic static final String TABLE_NAME = \"extended_properties\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Function.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**远程函数\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Function {\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/PgAttribute.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**PostgreSQL 字段属性\n * @author Lemon\n */\n@MethodAccess\npublic class PgAttribute {\n\tpublic static final String TAG = \"PgAttribute\";\n\tpublic static final String TABLE_NAME = \"pg_attribute\";\n\t\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/PgClass.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**PostgreSQL 表属性\n * @author Lemon\n */\n@MethodAccess\npublic class PgClass {\n\tpublic static final String TAG = \"PgClass\";\n\tpublic static final String TABLE_NAME = \"pg_class\";\n\t\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Request.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**请求处理\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Request {\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Script.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**代码脚本\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Script {\n}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/SysColumn.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 在 sys 下的字段(列名)\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class SysColumn {\n\tpublic static final String TAG = \"SysColumn\";\n\tpublic static final String TABLE_NAME = \"columns\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/SysTable.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**SQL Server 表属性\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class SysTable {\n\tpublic static final String TAG = \"SysTable\";\n\tpublic static final String TABLE_NAME = \"tables\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/Table.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport apijson.MethodAccess;\n\n/**表属性\n * @author Lemon\n */\n@MethodAccess(POST = {}, PUT = {}, DELETE = {})\npublic class Table {\n\tpublic static final String TAG = \"Table\";\n\tpublic static final String TABLE_NAME = \"tables\";\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/TestRecord.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\npackage apijson.orm.model;\n\nimport static apijson.orm.AbstractVerifier.ADMIN;\nimport static apijson.orm.AbstractVerifier.LOGIN;\n\nimport java.io.Serializable;\nimport java.sql.Timestamp;\n\nimport apijson.MethodAccess;\n\n/**测试结果。5.0.0 之后可能改名为 Test\n * @author Lemon\n */\n@MethodAccess(GET = { LOGIN, ADMIN }, HEAD = { LOGIN, ADMIN })\npublic class TestRecord implements Serializable {\n\t  private static final long serialVersionUID = 1L;\n\n\t  private Long id; //唯一标识\n\t  private Long userId; //用户id\n\t  private Long documentId; //测试用例文档id\n\t  private Timestamp date; //创建日期\n\t  private String compare; //对比结果\n\t  private String response; //接口返回结果JSON  用json格式会导致强制排序，而请求中引用赋值只能引用上面的字段，必须有序。\n\t  private String standard; //response 的校验标准，是一个 JSON 格式的 AST ，描述了正确 Response 的结构、里面的字段名称、类型、长度、取值范围 等属性。\n\n\n\t  public TestRecord() {\n\t    super();\n\t  }\n\t  public TestRecord(long id) {\n\t    this();\n\t    setId(id);\n\t  }\n\n\n\n\n\t  public Long getId() {\n\t    return id;\n\t  }\n\n\t  public TestRecord setId(Long id) {\n\t    this.id = id;\n\t    return this;\n\t  }\n\n\t  public Long getUserId() {\n\t    return userId;\n\t  }\n\n\t  public TestRecord setUserId(Long userId) {\n\t    this.userId = userId;\n\t    return this;\n\t  }\n\n\t  public Long getDocumentId() {\n\t    return documentId;\n\t  }\n\n\t  public TestRecord setDocumentId(Long documentId) {\n\t    this.documentId = documentId;\n\t    return this;\n\t  }\n\n\t  public Timestamp getDate() {\n\t    return date;\n\t  }\n\n\t  public TestRecord setDate(Timestamp date) {\n\t    this.date = date;\n\t    return this;\n\t  }\n\n\t  public String getCompare() {\n\t    return compare;\n\t  }\n\n\t  public TestRecord setCompare(String compare) {\n\t    this.compare = compare;\n\t    return this;\n\t  }\n\n\t  public String getResponse() {\n\t    return response;\n\t  }\n\n\t  public TestRecord setResponse(String response) {\n\t    this.response = response;\n\t    return this;\n\t  }\n\n\t  public String getStandard() {\n\t    return standard;\n\t  }\n\n\t  public TestRecord setStandard(String standard) {\n\t    this.standard = standard;\n\t    return this;\n\t  }\n\n\n\t}"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/model/package-info.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\n/**\n * models for special tables\n */\n/**\n * @author Lemon\n *\n */\npackage apijson.orm.model;"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/package-info.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\n/**\n * server files package\n */\n/**\n * @author Lemon\n *\n */\npackage apijson.orm;"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/script/JSR223ScriptExecutor.java",
    "content": "package apijson.orm.script;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\nimport javax.script.Bindings;\nimport javax.script.Compilable;\nimport javax.script.CompiledScript;\nimport javax.script.ScriptEngine;\nimport javax.script.ScriptEngineManager;\nimport javax.script.SimpleBindings;\n\nimport apijson.Log;\nimport apijson.orm.AbstractFunctionParser;\n\n/**\n * JSR223 script engine的统一实现抽象类\n */\npublic abstract class JSR223ScriptExecutor<T, M extends Map<String, Object>, L extends List<Object>> implements ScriptExecutor<T, M, L> {\n\tprivate static final String TAG = \"JSR223ScriptExecutor\";\n\n\tprotected ScriptEngine scriptEngine;\n\n\tprivate final Map<String, CompiledScript> compiledScriptMap = new ConcurrentHashMap<>();\n\t\n\t@Override\n\tpublic ScriptExecutor<T, M, L> init() {\n\t\tScriptEngineManager scriptEngineManager = new ScriptEngineManager();\n\t\tscriptEngine = scriptEngineManager.getEngineByName(scriptEngineName());\n\t\treturn this;\n\t}\n\n\tprotected abstract String scriptEngineName();\n\t\n\tprotected abstract Object extendParameter(AbstractFunctionParser<T, M, L> parser, Map<String, Object> currentObject, String methodName, Object[] args);\n\n\tprotected abstract boolean isLockScript(String methodName);\n\n\tprotected String convertScript(String script) {\n\t\treturn script;\n\t}\n\n\t@Override\n\tpublic void load(String name, String script) {\n\t\ttry {\n\t\t\tCompiledScript compiledScript = ((Compilable) scriptEngine).compile(convertScript(script));\n\t\t\tcompiledScriptMap.put(name, compiledScript);\n\t\t} catch (Exception e) {\n\t\t\tLog.e(TAG, \"compile script failed: \" + name, e);\n\t\t}\n\n\t}\n\n\t@Override\n\tpublic Object execute(AbstractFunctionParser<T, M, L> parser, Map<String, Object> currentObject, String methodName, Object[] args) throws Exception {\n\t\tCompiledScript compiledScript = compiledScriptMap.get(methodName);\n\t\tBindings bindings = new SimpleBindings();\n\t\t// 往脚本上下文里放入元数据\n\t\t// 把 RequestMethod method, String tag, int version, @NotNull JSONMap request,\n\t\t// HttpSession session 等参数作为全局参数传进去供脚本使用\n\t\t\n\t\t// 加载扩展属性\n\t\tObject extendParameter = this.extendParameter(parser, currentObject, methodName, args);\n\t\tif(extendParameter != null) {\n\t\t\tbindings.put(\"extParam\", extendParameter);\n\t\t}\n\t\t\n\t\tMap<String, Object> metaMap = new HashMap<>();\n\t\tmetaMap.put(\"version\", parser == null ? 0 : parser.getVersion());\n\t\tmetaMap.put(\"tag\", parser == null ? null : parser.getTag());\n\t\tmetaMap.put(\"args\", args);\n\t\tbindings.put(\"_meta\", metaMap);\n\t\treturn compiledScript.eval(bindings);\n\t}\n\n\t@Override\n\tpublic void cleanCache() {\n\t\tcompiledScriptMap.clear();\n\t}\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/script/JavaScriptExecutor.java",
    "content": "package apijson.orm.script;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.orm.AbstractFunctionParser;\n\n/**\n * JavaScript脚本语言的执行器实现\n */\npublic class JavaScriptExecutor<T, M extends Map<String, Object>, L extends List<Object>> extends JSR223ScriptExecutor<T, M, L> {\n\n    @Override\n    protected String scriptEngineName() {\n        return \"javascript\";\n    }\n\n\t@Override\n\tprotected Object extendParameter(AbstractFunctionParser<T, M, L> parser, Map<String, Object> currentObject, String methodName, Object[] args) {\n\t\treturn null;\n\t}\n\n\t@Override\n\tprotected boolean isLockScript(String methodName) {\n\t\treturn false;\n\t}\n\n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/orm/script/ScriptExecutor.java",
    "content": "package apijson.orm.script;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport apijson.orm.AbstractFunctionParser;\n\npublic interface ScriptExecutor<T, M extends Map<String, Object>, L extends List<Object>> {\n\n    ScriptExecutor<T, M, L> init();\n\n    void load(String name, String script);\n\n    Object execute(AbstractFunctionParser<T, M, L> parser, Map<String, Object> currentObject, String methodName, Object[] args) throws Exception;\n\n    void cleanCache();\n    \n}\n"
  },
  {
    "path": "APIJSONORM/src/main/java/apijson/package-info.java",
    "content": "/*Copyright (C) 2020 Tencent.  All rights reserved.\n\nThis source code is licensed under the Apache License Version 2.0.*/\n\n\n/**\n * the same files for server and client projects \n */\n/**\n * @author Lemon\n * \n */\npackage apijson;"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n我们提倡您通过提 Issue 和 Pull Request 方式来促进 APIJSON 的发展。\n\n\n## Acknowledgements\n\n非常感谢以下贡献者们对于 APIJSON 本项目做出的贡献：\n\n- [TommyLemon](https://github.com/TommyLemon)(腾讯工程师，还开源了 APIJSON-Demo, apijson-framework, apijson-column, apijson-router 等)\n- [ruoranw](https://github.com/ruoranw)(现居美国洛杉矶，还开源了 APIJSONdocs)\n- [zhoulingfengofcd](https://github.com/zhoulingfengofcd)\n- [Zerounary](https://github.com/Zerounary)(还开源了 APIJSONParser)\n- [fineday009](https://github.com/fineday009)(腾讯工程师，还贡献了 apijson-framework, APIJSON-Demo)\n- [vincentCheng](https://github.com/vincentCheng)(还开源了 apijson-doc)\n- [justinfengchen](https://github.com/justinfengchen)\n- [linlwqq](https://github.com/linlwqq)\n- [redcatmiss](https://github.com/redcatmiss)(社保科技工程师)\n- [linbren](https://github.com/linbren)\n- [jinzhongjian](https://github.com/jinzhongjian)\n- [CoolGeo2016](https://github.com/CoolGeo2016)\n- [1906522096](https://github.com/1906522096)\n- [github-ganyu](https://github.com/github-ganyu)\n- [sunxiaoguang](https://github.com/sunxiaoguang)(知乎基础研发架构师)\n- [403f](https://github.com/403f)\n- [gujiachun](https://github.com/gujiachun)\n- [gdjs2](https://github.com/gdjs2)(University of California, Riverside)\n- [Rkyzzy](https://github.com/Rkyzzy)(理想汽车工程师, SUSTech, University of California, Berkeley)\n- [kxlv2000](https://github.com/kxlv2000)(SUSTech)\n- [caohao-go](https://github.com/caohao-go)(腾讯工程师，曾在华为、恒生担任C/C++开发工程师，在wps担任项目经理，在360担任技术专家)\n- [Wscats](https://github.com/Wscats)(腾讯工程师、腾讯 AlloyTeam 成员、Tencent Creation Camp 成员、知名技术博主)\n- [jun0315](https://github.com/jun0315)(腾讯工程师)\n- [JieJo](https://github.com/JieJo)\n- [yeyuezhishou](https://github.com/yeyuezhishou)(圆通工程师)\n- [kenlig](https://github.com/kenlig)(还开源了 apijsondocs)\n- [andream7](https://github.com/andream7)(字节跳动、微软 工程师，还开源了 apijson-db2)\n- [qiujunlin](https://github.com/qiujunlin)(字节跳动工程师，还开源了 APIJSONDemo)\n- [HANXU2018](https://github.com/HANXU2018)(网易工程师，还开源了 APIJSON-DOC)\n- [hclown9804](https://github.com/hclown9804)(Datawhale)\n- [chenyanlann](https://github.com/chenyanlann)(还开源了 APIJSONDemo_ClickHouse)\n- [haolingzhang1](https://github.com/haolingzhang1)(腾讯工程师，还开源了 APIJson--demo)\n- [jerrylususu](https://github.com/jerrylususu)(还开源了 apijson_todo_demo 和 apijson_role_extend)\n- [Dalezee](https://github.com/Dalezee)(还开源了 apijson_camp)\n- [aaronlinv](https://github.com/aaronlinv)\n- [sy-records](https://github.com/sy-records)\n- [Finkyky](https://github.com/Finkyky)\n- [ifooling](https://github.com/ifooling)\n- [transtone](https://github.com/transtone)\n- [AwenJackson](https://github.com/AwenJackson)(上海信息出奇科技有限公司工程师，发布了 3 篇文章）\n- [andy19055](https://github.com/andy19055)\n- [glennliao](https://github.com/glennliao)(还开源了 [apijson-go](https://github.com/glennliao/apijson-go) 和 [apijson-go-ui](https://github.com/glennliao/apijson-go-ui)）\n- [eltociear](https://github.com/eltociear)\n- [wb04307201](https://github.com/wb04307201)(还开源了 [apijson-dynamic-datasource](https://github.com/wb04307201/apijson-dynamic-datasource)）\n- [cloudAndMonkey](https://github.com/cloudAndMonkey)(还贡献了 apijson-framework, APIJSON-Demo，新增支持了 Redis, Elasticsearch, Kafka, Lua 等并提供了多个 Demo）\n- [12345ZMTHL](https://github.com/12345ZMTHL)\n- [cnscoo](https://github.com/cnscoo)(阿里云工程师）\n- [aninZz](https://github.com/aninZz)\n- [leomiaomiao](https://github.com/leomiaomiao)\n- [YqxLzx](https://github.com/YqxLzx)\n- [hiteshbedre](https://github.com/hiteshbedre)(privado.ai 印裔工程师)\n- [wahowaho](https://github.com/wahowaho)\n- [jarrodquan](https://github.com/jarrodquan)\n- [gemufeng](https://github.com/gemufeng)(上海麦市工程师)\n- [komiblog](https://github.com/komiblog)\n- [ostrichManX](https://github.com/ostrichManX)\n- [jia199807](https://github.com/jia199807)\n- [zxcwindy](https://github.com/zxcwindy)\n- [afumu](https://github.com/afumu)(gorm-plus 作者)\n- [alittle-yu](https://github.com/alittle-yu)\n- [Damon Nicola](https://github.com/Reynold3D)\n- [calmcc](https://github.com/calmcc)\n- [lindaifeng](https://github.com/lindaifeng)\n- [DenineLu](https://github.com/DenineLu)(小红书工程师）\n- [wz11wz](https://github.com/wz11wz)\n- [GeXin97](https://github.com/GeXin97)\n- [yunjiao-source](https://github.com/yunjiao-source)(还开源了 [apijson-spring-boot](https://gitee.com/yunjiao-source/apijson-spring-boot))\n- [moxixi527](https://github.com/moxixi527)(热门技术博主)\n\n#### 其中特别致谢: <br/>\ncloudAndMonkey 提交的 11 个 Commits, 对 APIJSON 做出了 1,496 增加和 845 处删减(截止 2022/12/15 日)； <br/>\njustinfengchen 提交的 6 个 Commits, 对 APIJSON 做出了 3,130 增加和 0 处删减(截止 2020/11/04 日)； <br/>\nruoranw 提交的 18 个 Commits, 对 APIJSON 做出了 328 增加和 520 处删减(截止 2020/11/04 日)； <br/>\nZerounary 提交的 6 个 Commits, 对 APIJSON 做出了 1,104 增加和 1 处删减(截止 2020/11/04 日)。 <br/>\n\n<br/>\nAPIJSON 持续招募贡献者，新增功能、修复 Bug、完善文档、修正错误、宣传推广、回答问题等，都能帮助项目及广大用户。 <br/>\nAPIJSON 已开发近 6 年，在此感谢所有开发者对于 APIJSON 的喜欢和支持，希望你能够成为 APIJSON 的核心贡献者， <br/>\n加入 APIJSON ，共同打造一个更棒的零代码、全功能、强安全 ORM 库，造福更多前后端开发者！🍾🎉\n\n### 为什么一定要贡献代码？\nAPIJSON 作为腾讯开源的知名热门项目，贡献代码除了可以给简历加亮点、为面试加分，还可以避免你碰到以下麻烦： <br/>\n1.你在 APIJSON 上更改的代码其他人看不到，不能帮你发现 Bug，更不可能帮你修复 Bug 甚至优化代码 <br/>\n2.作者和其它贡献者可能不兼容你更改的代码，导致你的项目在升级 APIJSON 版本后在功能甚至编译上出错 <br/>\n3.你需要自己维护你的代码，每次升级 APIJSON 版本时，你都需要下载 APIJSON 新代码再合并你自己的更改 <br/>\n#### 所以为了让你自己的更改始终能跟上项目版本，得到他人给予的可靠且持续的维护，强烈建议 [提交 Pull Request](/CONTRIBUTING.md#pull-request) 来贡献代码。\n\n​             \n\n##  Pull Request\n\n我们除了希望听到您的反馈和建议外，我们也希望您接受代码形式的直接帮助，对我们的 GitHub 发出 Pull Request 请求。\n\n### 如果是小改文档或代码\n\n直接点文件右上角的编辑图标按钮 <br/> \n![image](https://user-images.githubusercontent.com/5738175/130585672-8bd49ae5-2978-4ad6-a7a6-de0a0c2d0b68.png)\n\n<br/>\n\n然后底部简要输入修改说明，点击 Commit Change 按钮 <br/> \n![image](https://user-images.githubusercontent.com/5738175/130586073-4a6aea74-3c88-4cd9-9c93-ffaba1270ab8.png)\n\n\n### 如果有比较大的改动\n\n以下是具体步骤：(如果使用本步骤，GitHub 可能不会把贡献者添加到 Contributors 中，推荐用以下 [详细的图文步骤](https://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md#%E8%AF%A6%E7%BB%86%E7%9A%84%E5%9B%BE%E6%96%87%E6%AD%A5%E9%AA%A4%E5%8F%AF%E5%8F%82%E8%80%83%E4%BB%A5%E4%B8%8B%E4%BB%BB%E6%84%8F%E4%B8%80%E7%AF%87))\n\n#### Fork 仓库\n\n点击 `Fork` 按钮，将需要参与的项目仓库 Fork 到自己的 Github 中。\n\n#### Clone 已 Fork 项目\n\n在自己的 Github 中，找到 Fork 下来的项目，git clone 到本地。\n\n```bash\n$ git clone git@github.com:<yourname>/APIJSON.git\n```\n\n#### 添加 APIJSON 仓库\n\n将 Fork 源仓库连接到本地仓库：\n\n```bash\n$ git remote add <name> <url>\n# 例如：\n$ git remote add APIJSON git@github.com:Tencent/APIJSON.git\n```\n\n#### 保持与 APIJSON 仓库的同步\n\n直接在 fork Repo 的首页点 Contribute > Open pull request\n\n![image](https://user-images.githubusercontent.com/5738175/131776033-74caf279-ebbf-45f1-a9c1-beff937a87fb.png)\n\n或者\n```bash\n$ git pull --rebase <name> <branch>\n# 等同于以下两条命令\n$ git fetch <name> <branch>\n$ git rebase <name>/<branch>\n```\n\n#### Commit 信息提交\n\nCommit 信息请遵循 [Commit 消息约定](./CONTRIBUTING_COMMIT.md)，以便可以自动生成 `CHANGELOG` 。具体格式请参考 Commit 文档规范。\n\n<br/><br/>\n \n#### 详细的图文步骤可参考以下任意一篇\nGitHub - 对项目做出贡献 <br/>\nhttps://www.jianshu.com/p/00cf29d2d66c\n<br/><br/>\n如何在 Github 上给别人的项目贡献代码 <br/>\nhttps://git-scm.com/book/zh/v2/GitHub-%E5%AF%B9%E9%A1%B9%E7%9B%AE%E5%81%9A%E5%87%BA%E8%B4%A1%E7%8C%AE\n\n\n​                       \n\n## Issue 提交\n\n#### 对于贡献者\n\n在提 Issue 前请确保满足一下条件：\n\n- 必须是一个 Bug 或者功能新增。\n- 必须是 APIJSON 相关问题。\n- 已经在 Issue 中搜索过，并且没有找到相似的 Issue 或者解决方案。\n- 完善下面模板中的信息\n\n如果已经满足以上条件，我们提供了 Issue 的标准模版，请按照模板填写。\n"
  },
  {
    "path": "CONTRIBUTING_COMMIT.md",
    "content": "# Commit 规范\n\n在对项目作出更改后，我们需要生成 Commit 来记录自己的更改。以下是参照 Angular 对 Commit 格式的规范：\n\n## (1) 格式\n\n提交信息包括三个部分：`Header`，`Body` 和 `Footer`。\n\n```\n<Header>\n\n<Body>\n\n<Footer>\n```\n\n其中，Header 是必需的，Body 和 Footer 可以省略。\n\n#### 1> Header\n\nHeader 部分只有一行，包括俩个字段：`type`（必需）和`subject`（必需）。\n\n```\n<type>: <subject>\n```\n\n**Type**\n\nType 用于说明 Commit 的类别，可以使用如下类别：\n\n- feat：新功能（Feature）\n- fix：修补 Bug\n- doc：文档（Documentation）\n- style： 格式（不影响代码运行的变动）\n- refactor：重构（即不是新增功能，也不是修改 Bug 的代码变动）\n- test：增加测试\n- chore：构建过程或辅助工具的变动\n\n**Subject**\n\nSubject 是 Commit 目的的简短描述。\n\n- 以动词开头，使用第一人称现在时，比如改变，而不是改变了。\n- 结尾不加句号（。）\n\n#### 2> Body\n\nBody 部分是对本次 Commit 的详细描述，可以分成多行。下面是一个范例。\n\n```\nMore detailed explanatory text, if necessary.  Wrap it to \nabout 72 characters or so. \n\nFurther paragraphs come after blank lines.\n\n- Bullet points are okay, too\n- Use a hanging indent\n```\n\n**注意：**应该说明代码变动的动机，以及与以前行为的对比。\n\n#### 3> Footer\n\n​\tFooter 部分应该包含：(1)Breaking Changes;  (2)关闭 Issue；\n\n​\t**Breaking Changes**：\n\n​\t如果当前代码与上一个版本不兼容，则 Footer 部分以`BREAKING CHANGE`开头，后面是对变动的描述、以及变动理由和迁移方法。这种使用较少，了解即可。\n\n​\t**Issue 部分：**\n\n- 通过 Commit 关联 Issue：\n\n  如果当前提交信息关联了某个 Issue，那么可以在 Footer 部分关联这个 Issue：\n\n  ```\n  issue #2\n  ```\n\n- 通过 Commit 关闭 Issue，当提交到**默认分支**时，提交信息里可以使用 `fix/fixes/fixed` ， `close/closes/closed` 或者 `resolve/resolves/resolved`等关键词，后面再跟上 Issue 号，这样就会关闭这个 Issue：\n\n```\ncloses #1\n```\n\n​\t注意，如果不是提交到默认分支，那么并不能关闭这个 Issue，但是在这个 Issue 下面会显示相关的信息表示曾经想要关闭这个 Issue，当这个分支合并到默认分支时，就可以关闭这个 Issue 了。\n\n#### 4> 例子\n\n下面是一个完整的例子：\n\n```\nfeat: 添加了分享功能\n\n给每篇文章添加了分享功能\n\n- 添加分享到微信功能\n- 添加分享到朋友圈功能\n\nissue #1, #2\ncloses #1\n```\n"
  },
  {
    "path": "Document-Chinese.md",
    "content": "## 中文 | [English](https://github.com/Tencent/APIJSON/blob/master/Document.md)\n\n# APIJSON 通用文档 \n本文是通用文档，只和 APIJSON 协议有关，和 C#, Go, Java, JavaScript, PHP, Python, TypeScript 等开发语言无关。 <br />\n具体开发语言相关的 配置、运行、部署 等文档见各个相关项目的文档，可以在首页点击对应语言的入口来查看。<br />\nhttps://github.com/Tencent/APIJSON\n![image](https://user-images.githubusercontent.com/5738175/134520081-a63d3817-321c-4e7b-9e03-73c6827a19c1.png)\n\n\n后端开发者可以先看 [图文入门教程1](http://apijson.cn/doc/zh/) 或 [图文入门教程2](https://hanxu2018.github.io/APIJSON-DOC/) （和本文档有出入的点以本文档为准。例如正则匹配 key? 已废弃，用 key~ 替代；例如 \"@column\":\"store_id,sum(amt):totAmt\" 中逗号 , 有误，应该用分号 ; 隔开 SQL 函数，改为 \"@column\":\"store_id;sum(amt):totAmt\"）\n\n* ### [1.示例](#1)\n* ### [2.对比传统方式](#2)\n* [2.1 开发流程](#2.1)\n* [2.2 前端请求](#2.2)\n* [2.3 后端操作](#2.3)\n* [2.4 前端解析](#2.4)\n* [2.5 对应不同需求的请求](#2.5)\n* [2.6 对应不同请求的结果](#2.6)\n* ### [3.设计规范](#3)\n* [3.1 操作方法](#3.1)\n* [3.2 功能符](#3.2)\n\n\n## <h2 id=\"1\">1.示例<h2/>\n\n#### 获取用户\n请求：\n<pre><code class=\"language-json\">{\n  \"User\":{\n    \"id\":38710\n  }\n}\n</code></pre>\n\n[点击这里测试](http://apijson.cn:8080/get/{\"User\":{\"id\":38710}})\n\n返回：\n<pre><code class=\"language-json\">{\n  \"User\":{\n    \"id\":38710,\n    \"sex\":0,\n    \"name\":\"TommyLemon\",\n    \"tag\":\"Android&Java\",\n    \"head\":\"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n    \"date\":1485948110000,\n    \"pictureList\":[\n      \"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n      \"http://common.cnblogs.com/images/icon_weibo_24.png\"\n    ]\n  },\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON 各种单表对象查询：简单查询、统计、分组、排序、聚合、比较、筛选字段、字段别名 等</a>\n</p> \n  \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_single.gif)\n  \n<br />\n\n#### 获取用户列表\n请求：\n<pre><code class=\"language-json\">{\n  \"[]\":{\n    \"count\":3, //只要3个\n    \"User\":{\n      \"@column\":\"id,name\" //只要id,name这两个字段\n    }\n  }\n}\n</code></pre>\n\n[点击这里测试](http://apijson.cn:8080/get/{\"[]\":{\"count\":3,\"User\":{\"@column\":\"id,name\"}}})\n\n返回：\n<pre><code class=\"language-json\">{\n  \"[]\":[\n    {\n      \"User\":{\n        \"id\":38710,\n        \"name\":\"TommyLemon\"\n      }\n    },\n    {\n      \"User\":{\n        \"id\":70793,\n        \"name\":\"Strong\"\n      }\n    },\n    {\n      \"User\":{\n        \"id\":82001,\n        \"name\":\"Android\"\n      }\n    }\n  ],\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON 各种单表数组查询：简单查询、统计、分组、排序、聚合、分页、比较、搜索、正则、条件组合 等</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_array.gif)\n\n<br />\n\n#### 获取动态及发布者用户\n请求：\n<pre><code class=\"language-json\">{\n  \"Moment\":{\n  },\n  \"User\":{\n    \"id@\":\"Moment/userId\"  //User.id = Moment.userId\n  }\n}\n</code></pre>\n\n[点击这里测试](http://apijson.cn:8080/get/{\"Moment\":{},\"User\":{\"id@\":\"Moment%252FuserId\"}})\n\n返回：\n<pre><code class=\"language-json\">{\n  \"Moment\":{\n    \"id\":12,\n    \"userId\":70793,\n    \"date\":\"2017-02-08 16:06:11.0\",\n    \"content\":\"1111534034\"\n  },\n  \"User\":{\n    \"id\":70793,\n    \"sex\":0,\n    \"name\":\"Strong\",\n    \"tag\":\"djdj\",\n    \"head\":\"http://static.oschina.net/uploads/user/585/1170143_50.jpg?t=1390226446000\",\n    \"contactIdList\":[\n      38710,\n      82002\n    ],\n    \"date\":\"2017-02-01 19:21:50.0\"\n  },\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n  \n<br />\n\n#### 获取类似微信朋友圈的动态列表\n请求：\n<pre><code class=\"language-json\">{\n  \"[]\":{                             //请求一个数组\n    \"page\":0,                        //数组条件\n    \"count\":2,\n    \"Moment\":{                       //请求一个名为Moment的对象\n      \"content$\":\"%a%\"               //对象条件，搜索content中包含a的动态\n    },\n    \"User\":{\n      \"id@\":\"/Moment/userId\",  //User.id = Moment.userId  缺省引用赋值路径，从所处容器的父容器路径开始\n      \"@column\":\"id,name,head\"       //指定返回字段\n    },\n    \"Comment[]\":{                    //请求一个名为Comment的数组，并去除Comment包装\n      \"count\":2,\n      \"Comment\":{\n        \"momentId@\":\"[]/Moment/id\"   //Comment.momentId = Moment.id  完整引用赋值路径\n      }\n    }\n  }\n}\n</code></pre>\n\n[点击这里测试](http://apijson.cn:8080/get/{\"[]\":{\"page\":0,\"count\":2,\"Moment\":{\"content$\":\"%2525a%2525\"},\"User\":{\"id@\":\"%252FMoment%252FuserId\",\"@column\":\"id,name,head\"},\"Comment[]\":{\"count\":2,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n\n返回：\n<pre><code class=\"language-json\">{\n  \"[]\":[\n    {\n      \"Moment\":{\n        \"id\":15,\n        \"userId\":70793,\n        \"date\":1486541171000,\n        \"content\":\"APIJSON is a JSON Transmission Protocol…\",\n        \"praiseUserIdList\":[\n          82055,\n          82002,\n          82001\n        ],\n        \"pictureList\":[\n          \"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n          \"http://common.cnblogs.com/images/icon_weibo_24.png\"\n        ]\n      },\n      \"User\":{\n        \"id\":70793,\n        \"name\":\"Strong\",\n        \"head\":\"http://static.oschina.net/uploads/user/585/1170143_50.jpg?t=1390226446000\"\n      },\n      \"Comment[]\":[\n        {\n          \"id\":176,\n          \"toId\":166,\n          \"userId\":38710,\n          \"momentId\":15,\n          \"date\":1490444883000,\n          \"content\":\"thank you\"\n        },\n        {\n          \"id\":1490863469638,\n          \"toId\":0,\n          \"userId\":82002,\n          \"momentId\":15,\n          \"date\":1490863469000,\n          \"content\":\"Just do it\"\n        }\n      ]\n    },\n    {\n      \"Moment\":{\n        \"id\":58,\n        \"userId\":90814,\n        \"date\":1485947671000,\n        \"content\":\"This is a Content...-435\",\n        \"praiseUserIdList\":[\n          38710,\n          82003,\n          82005,\n          93793,\n          82006,\n          82044,\n          82001\n        ],\n        \"pictureList\":[\n          \"http://static.oschina.net/uploads/img/201604/22172507_aMmH.jpg\"\n        ]\n      },\n      \"User\":{\n        \"id\":90814,\n        \"name\":7,\n        \"head\":\"http://static.oschina.net/uploads/user/51/102723_50.jpg?t=1449212504000\"\n      },\n      \"Comment[]\":[\n        {\n          \"id\":13,\n          \"toId\":0,\n          \"userId\":82005,\n          \"momentId\":58,\n          \"date\":1485948050000,\n          \"content\":\"This is a Content...-13\"\n        },\n        {\n          \"id\":77,\n          \"toId\":13,\n          \"userId\":93793,\n          \"momentId\":58,\n          \"date\":1485948050000,\n          \"content\":\"This is a Content...-77\"\n        }\n      ]\n    }\n  ],\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON 各种多表关联查询：一对一、一对多、多对一、各种条件 等</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_associate.gif)\n\n<br />\n  \n<p align=\"center\" >\n  <a >[GIF] APIJSON 各种 JOIN：< LEFT JOIN, & INNER JOIN 等</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_join.gif)\n  \n<br />\n  \n<p align=\"center\" >\n  <a >[GIF] APIJSON 各种子查询：@from@ FROM, key@ =, key>@ >, key{}@ IN, key}{@ EXISTS 等 </a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_subquery.gif)\n    \n<br />\n    \n<p align=\"center\" >\n  <a >[GIF] APIJSON 部分功能演示集合，由浅入深、由简单到复杂 </a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_summary.gif)\n\n<br />\n\n[在线测试](http://apijson.cn/api)\n\n<br />\n<br />\n \n## <h2 id=\"2\">2.对比传统RESTful方式<h2/>\n\n### <h3 id=\"2.1\">2.1 开发流程<h3/>\n 开发流程 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n 接口传输 | 等后端编辑接口，然后更新文档，前端再按照文档编辑请求和解析代码 | 前端按照自己的需求编辑请求和解析代码。<br />没有接口，更不需要文档！前端再也不用和后端沟通接口或文档问题了！\n 兼容旧版 | 后端增加新接口，用v2表示第2版接口，然后更新文档 | 什么都不用做！\n \n <br />\n \n### <h3 id=\"2.2\">2.2 前端请求<h3/>\n 前端请求 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n 要求 | 前端按照文档在对应URL后面拼接键值对 | 前端按照自己的需求在固定URL后拼接JSON\n URL | 不同的请求对应不同的URL，基本上有多少个不同的请求就得有多少个接口URL | 相同的操作方法(增删改查)都用同一个URL，<br />大部分请求都用7个通用接口URL的其中一个\n 键值对 | key=value | key:value\n 结构 | 同一个URL内table_name只能有一个 <br /><br /> base_url/get/table_name?<br />key0=value0&key1=value1... | 同一个URL后TableName可传任意数量个 <br /><br /> base_url/get/<br />{<br > &nbsp;&nbsp; TableName0:{<br > &nbsp;&nbsp;&nbsp;&nbsp; key0:value0,<br > &nbsp;&nbsp;&nbsp;&nbsp; key1:value1,<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; TableName1:{<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; }<br > &nbsp;&nbsp; ...<br > }\n \n <br />\n \n### <h3 id=\"2.3\">2.3 后端操作<h3/>\n 后端操作 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n 解析和返回 | 取出键值对，把键值对作为条件用预设的的方式去查询数据库，最后封装JSON并返回给前端 | 把Parser#parse方法的返回值返回给前端就行\n 返回JSON结构的设定方式 | 由后端设定，前端不能修改 | 由前端设定，后端不能修改\n \n <br />\n \n### <h3 id=\"2.4\">2.4 前端解析<h3/>\n 前端解析 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n 查看方式 | 查文档或问后端，或等请求成功后看日志 | 看请求就行，所求即所得，不用查、不用问、不用等。也可以等请求成功后看日志\n 解析方法 | 用JSON解析器来解析JSONObject | 可以用JSONResponse解析JSONObject，或使用传统方式\n \n <br />\n \n### <h3 id=\"2.5\">2.5 前端对应不同需求的请求<h3/>\n 前端的请求 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n User | base_url/get/user?id=38710 | [base_url/get/<br >{<br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"User\":{\"id\":38710}})\n Moment和对应的User | 分两次请求<br />Moment: <br /> base_url/get/moment?userId=38710<br /><br />User: <br /> base_url/get/user?id=38710 | [base_url/get/<br >{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710<br > &nbsp;&nbsp; }, <br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"Moment\":{\"userId\":38710},\"User\":{\"id\":38710}})\n User列表 | base_url/get/user/list?<br />page=0&count=3&sex=0 | [base_url/get/<br >{<br > &nbsp;&nbsp; \"User[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0,<br > &nbsp;&nbsp;&nbsp;&nbsp;  \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"sex\":0<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"User[]\":{\"page\":0,\"count\":3,\"User\":{\"sex\":0}}})\n Moment列表，<br />每个Moment包括<br />1.发布者User<br />2.前3条Comment | Moment里必须有<br />1.User对象<br >2.Comment数组<br /><br /> base_url/get/moment/list?<br />page=0&count=3&commentCount=3 | [base_url/get/<br >{<br > &nbsp;&nbsp; \"[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{}, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"count\":3,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"[]/Moment/id\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"[]\":{\"page\":0,\"count\":3,\"Moment\":{},\"User\":{\"id@\":\"%252FMoment%252FuserId\"},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n User发布的Moment列表，<br /> 每个Moment包括<br /> 1.发布者User<br /> 2.前3条Comment | 1.Moment里必须有User对象和Comment数组<br > 2.字段名必须查接口文档，例如评论数量字段名可能是<br /> commentCount,comment_count或者简写cmt_count等各种奇葩写法... <br /><br /> base_url/get/moment/list?<br />page=0&count=3<br />&commentCount=3&userId=38710 | 有以下几种方式:<br /><br /> ① 把以上请求里的<br >\"Moment\":{}, \"User\":{\"id@\":\"/Moment/userId\"}<br >改为<br >[\"Moment\":{\"userId\":38710}, \"User\":{\"id\":38710}](http://apijson.cn:8080/get/{\"[]\":{\"page\":0,\"count\":3,\"Moment\":{\"userId\":38710},\"User\":{\"id\":38710},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}}) <br /><br /> ② 或把User放在上面的最外层省去重复的User<br />[base_url/get/<br >{<br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710<br > &nbsp;&nbsp;&nbsp;&nbsp; }, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"count\":3,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"[]/Moment/id\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"User\":{\"id\":38710},\"[]\":{\"page\":0,\"count\":3,\"Moment\":{\"userId\":38710},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})<br /><br /> ③ 如果User之前已经获取到了，还可以不传User来节省请求和返回数据的流量并提升速度<br />[base_url/get/<br >{<br > &nbsp;&nbsp; \"[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"count\":3,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"[]/Moment/id\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"[]\":{\"page\":0,\"count\":3,\"Moment\":{\"userId\":38710},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n \n <br />\n \n### <h3 id=\"2.6\">2.6 后端对应不同请求的返回结果<h3/>\n 后端的返回结果 | 传统方式 | APIJSON\n-------- | ------------ | ------------\n User | {<br > &nbsp;&nbsp; \"data\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} | {<br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\n Moment和对应的User | 分别返回两次请求的结果，获取到Moment后取出userId作为User的id条件去查询User <br /><br /> Moment: <br > {<br > &nbsp;&nbsp; \"data\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} <br /><br /> User: <br > {<br > &nbsp;&nbsp; \"data\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} | 一次性返回，没有传统方式导致的 长时间等待结果、两次结果间关联、线程多次切换 等问题 <br /><br /> {<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\n User列表 | {<br > &nbsp;&nbsp; \"data\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":82001,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} | {<br > &nbsp;&nbsp; \"User[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":82001,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\n Moment列表，每个Moment包括发布者User和前3条Comment | Moment里必须有<br />1.User对象<br />2.Comment数组 <br /><br /> {<br > &nbsp;&nbsp; \"data\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":301,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} | 1.高灵活，可任意组合<br />2.低耦合，逻辑很清晰<br /><br />{<br > &nbsp;&nbsp; \"[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":301,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\n User发布的Moment列表，每个Moment包括发布者User和前3条Comment | 1.大量重复User，浪费流量和服务器性能<br />2.优化很繁琐，需要后端扩展接口、写好文档，前端/前端再配合优化<br /><br />{<br > &nbsp;&nbsp; \"data\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":470,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":511,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":595,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} | 以上不同请求方式的结果:<br /><br /> ① 常规请求 <br > {<br > &nbsp;&nbsp; \"[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br /><br /> ② 省去重复的User <br > {<br > &nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"name\":\"Tommy\",<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br /><br /> ③ 不查询已获取到的User <br > {<br > &nbsp;&nbsp; \"[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp; {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"xxx\",<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":[<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ]<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\n\n\n1.base_url指基地址，一般是顶级域名，其它分支url都是在base_url后扩展。如base_url:http://apijson.cn:8080/ ，对应的GET分支url:http://apijson.cn:8080/get/ 。下同。<br >\n2.请求中的key或value任意一个为null值时，这个 key:value键值对 被视为无效。下同。<br >\n3.请求中的 / 需要转义。JSONRequest.java已经用URLEncoder.encode转义，不需要再写；但如果是浏览器或Postman等直接输入url/request，需要把request中的所有 / 都改成 %252F 。下同。<br >\n4.code，指返回结果中的状态码，200表示成功，其它都是错误码，值全部都是HTTP标准状态码。下同。<br >\n5.msg，指返回结果中的状态信息，对成功结果或错误原因的详细说明。下同。<br >\n6.code和msg总是在返回结果的同一层级成对出现。对所有请求的返回结果都会在最外层有一对总结式code和msg。下同。<br >\n7.id等字段对应的值仅供说明，不一定是数据库里存在的，请求里用的是真实存在的值。下同。\n\n<br />\n<br />\n\n## <h2 id=\"3\">3.设计规范<h2/>\n\n### <h3 id=\"3.1\">3.1 操作方法<h3/>\n\n 方法及说明 | URL | Request | Response\n------------ | ------------ | ------------ | ------------\nGET: <br > 普通获取数据，<br > 可用浏览器调试 | base_url/get/ | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; … <br > &nbsp;&nbsp; }<br >} <br > {…}内为限制条件<br ><br > 例如获取一个 id = 235 的 Moment：<br > [{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235<br > &nbsp;&nbsp; }<br >}](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fget&type=JSON&json={\"Moment\"%3A{\"id\"%3A235}}) <br > 后端校验通过后自动解析为 SQL 并执行：<br >`SELECT * FROM Moment WHERE id=235 LIMIT 1` | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; ...<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br >例如<br >{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br > }\nHEAD: <br > 普通获取数量，<br > 可用浏览器调试 | base_url/head/ | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp; }<br > } <br > {…}内为限制条件 <br ><br > 例如获取一个 id = 38710 的 User 所发布的 Moment 总数：<br > [{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710<br > &nbsp;&nbsp; }<br >}](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fhead&type=JSON&json={\"Moment\"%3A{\"userId\"%3A38710}}) <br > 后端校验通过后自动解析为 SQL 并执行：<br >`SELECT count(*) FROM Moment WHERE userId=38710 LIMIT 1`  | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":10<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} <br > 例如<br >{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":10<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;  \"msg\":\"success\"<br >}\nGETS: <br > 安全/私密获取数据，<br > 用于获取钱包等<br >对安全性要求高的数据 | base_url/gets/ | 最外层加一个 \"tag\":tag，例如 [\"tag\":\"Privacy\"](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={\"tag\"%3A\"Privacy\",\"Privacy\"%3A{\"id\"%3A82001}})，其它同GET | 同GET\nHEADS: <br > 安全/私密获取数量，<br > 用于获取银行卡数量等<br >对安全性要求高的数据总数 | base_url/heads/ | 最外层加一个 \"tag\":tag，例如 [\"tag\":\"Verify\"](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fheads&type=JSON&json={\"tag\"%3A\"Verify\",\"Verify\"%3A{\"phone\"%3A13000082001}})，其它同HEAD | 同HEAD\nPOST: <br > 新增数据 | base_url/post/ | 单个： <br > {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":tag<br >} <br > {…}中id由后端生成，不能传 <br ><br >例如当前登录用户 38710 发布一个新 Comment：<br >[{<br > &nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"momentId\":12,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\" <br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":\"Comment\"<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fpost&type=JSON&json={\"Comment\":{\"momentId\":12,\"content\":\"APIJSON%20is%20the%20Real-Time%20coding-free,%20powerful%20and%20secure%20ORM.\"},\"tag\":\"Comment\"}) <br > 后端校验通过后自动解析为 SQL 并执行：<br >`INSERT INTO Comment(userId,momentId,content) VALUES(38710,12,'APIJSON is the real-time coding-free, powerful and secure ORM')` <br >  <br > 批量： <br > {<br > &nbsp;&nbsp; TableName\\[]:\\[{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp;&nbsp;&nbsp; }, {<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp; ],<br > &nbsp;&nbsp; \"tag\":tag<br >} <br > {…}中id由后端生成，不能传 <br ><br >例如当前登录用户 82001 发布 2 个 Comment：<br >[{<br > &nbsp;&nbsp; \"Comment[]\":[{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"momentId\":12,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br > &nbsp;&nbsp;&nbsp;&nbsp; }, {<br > &nbsp;&nbsp;&nbsp;&nbsp; \"momentId\":15,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is a JSON transmision protocol.\"<br > &nbsp;&nbsp; }],<br > &nbsp;&nbsp; \"tag\":\"Comment:[]\"<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fpost&type=JSON&json={\"Comment[]\":[{\"momentId\":12,\"content\":\"APIJSON%20is%20the%20Real-Time%20coding-free,%20powerful%20and%20secure%20ORM.\"},{\"momentId\":15,\"content\":\"APIJSON%20is%20a%20JSON%20transmision%20protocol.\"}],\"tag\":\"Comment:[]\"})<br >  后端校验通过后自动解析为 SQL 并执行：<br >`INSERT INTO Comment(userId,momentId,content) VALUES(82001,12,'APIJSON is the real-time coding-free, powerful and secure ORM');`<br ><br >`INSERT INTO Comment(userId,momentId,content) VALUES(82001,15,'APIJSON is a JSON transmision protocol.');`   | 单个： <br > {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br >例如<br >{<br > &nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":120<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >} <br > <br > 批量： <br > {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":5,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id[]\":[1, 2, 3, 4, 5]<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br >例如<br >{<br > &nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":2,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id[]\":\\[1, 2]<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}\nPUT: <br > 修改数据，<br > 只修改所传的字段 | base_url/put/ | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":id,<br > &nbsp;&nbsp;&nbsp;&nbsp; …<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":tag<br >} <br > {…} 中 id 或 id{} 至少传一个 <br ><br >例如当前登录用户 82001 修改 id = 235 的 Moment 的 content：<br >[{<br > &nbsp;&nbsp; \"Moment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":\"Moment\"<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fput&type=JSON&json={\"Moment\":{\"id\":235,\"content\":\"APIJSON%20is%20the%20Real-Time%20coding-free,%20powerful%20and%20secure%20ORM.\"},\"tag\":\"Moment\"}) <br > 后端校验通过后自动解析为 SQL 并执行：<br >`UPDATE Moment SET content='APIJSON is the real-time coding-free, powerful and secure ORM' WHERE id=235 AND userId=82001 LIMIT 1` <br ><br > 批量除了 id{}:\\[] 也可类似批量 POST，只是每个 {...} 里面都必须有 id。<br >\"tag\":\"Comment[]\" 对应对象 \"Comment\":{\"id{}\":[1,2,3]}，表示指定记录全部统一设置；<br >\"tag\":\"Comment:[]\" 多了冒号，对应数组 \"Comment[]\":[{\"id\":1},{\"id\":2},{\"id\":3}]，表示每项单独设置 | 同POST\nDELETE: <br > 删除数据 | base_url/delete/ | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id\":id<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":tag<br >} <br > {…} 中 id 或 id{} 至少传一个，一般只传 id 或 id{} <br ><br >例如当前登录用户 82001 批量删除 id = 100,110,120 的 Comment：<br >[{<br > &nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id{}\":[100,110,120]<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"tag\":\"Comment[]\"<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fdelete&type=JSON&json={\"Comment\":{\"id{}\":[100,110,120]},\"tag\":\"Comment[]\"}) <br > 后端校验通过后自动解析为 SQL 并执行：<br >`DELETE FROM Comment WHERE id IN(100,110,120) AND userId=82001 LIMIT 3` | {<br > &nbsp;&nbsp; TableName:{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id[]\":[100,110,120]<br >&nbsp;&nbsp; &nbsp;&nbsp; \"count\":3<br > &nbsp;&nbsp; },<br > &nbsp;&nbsp; \"code\":200,<br > &nbsp;&nbsp; \"msg\":\"success\"<br >}<br >例如<br >{<br >&nbsp;&nbsp; \"Comment\":{<br >&nbsp;&nbsp; &nbsp;&nbsp; \"code\":200,<br >&nbsp;&nbsp; &nbsp;&nbsp; \"msg\":\"success\",<br >&nbsp;&nbsp; &nbsp;&nbsp; \"id[]\":[100,110,120],<br >&nbsp;&nbsp; &nbsp;&nbsp; \"count\":3<br >&nbsp;&nbsp; },<br >&nbsp;&nbsp; \"code\":200,<br >&nbsp;&nbsp; \"msg\":\"success\"<br >}\n以上接口的简单形式: <br > base_url/{method}/{tag} |  GET: 普通获取数据 <br > base_url/get/{tag} <br ><br > HEAD: 普通获取数量 <br > base_url/head/{tag} <br ><br > GETS: 安全/私密获取数据 <br >  base_url/gets/{tag} <br ><br > HEADS: 安全/私密获取数量 <br > base_url/heads/{tag} <br ><br > POST: 新增数据 <br >  base_url/post/{tag} <br ><br > PUT: 修改数据  base_url/put/{tag} <br ><br > DELETE: 删除数据 <br > base_url/delete/{tag} | 例如安全/私密获取一个 id = 82001 的 Privacy：<br > [base_url/gets/Privacy/ <br > {\"id\":82001}](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets%2FPrivacy&type=JSON&json={\"id\"%3A82001}) <br > 相当于 <br > [base_url/gets/ <br >{\"tag\":\"Privacy\", \"Privacy\":{\"id\":82001}}](http://apijson.cn/api/?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={\"tag\"%3A\"Privacy\",\"Privacy\"%3A{\"id\"%3A82001}}) <br > <br > 例如批量修改 id = 114, 124 的 Comment 的 content：<br >[base_url/put/Comemnt[]/ <br > {<br > &nbsp;&nbsp; \"id{}\":[114,124],<br > &nbsp;&nbsp; \"content\":\"test multi put\"<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fput%2FComment[]&type=JSON&json={\"id{}\"%3A[114,124],\"content\"%3A\"test%20multi%20put\"}) <br > 相当于 <br > [base_url/put/ <br > {<br > &nbsp;&nbsp; \"tag\":\"Comment[]\", <br > &nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"id{}\":[114,124],<br > &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"test multi put\"<br > &nbsp;&nbsp; }<br >}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fput&type=JSON&json={\"tag\"%3A\"Comment[]\",\"Comment\"%3A{\"id{}\"%3A[114,124],\"content\"%3A\"test%20multi%20put\"}})  | 同以上对应的方法\n\n1.TableName指要查询的数据库表Table的名称字符串。第一个字符为大写字母，剩下的字符要符合英语字母、数字、下划线中的任何一种。对应的值的类型为JSONObject，结构是 {...}，里面放的是Table的字段(列名)。下同。<br >\n2.\"tag\":tag 后面的tag是非GET、HEAD请求中匹配请求的JSON结构的标识，一般是要查询的Table的名称，由后端Request表中指定。下同。<br >\n3.GET、HEAD请求是开放请求，可任意组合任意嵌套。其它请求为受限制的安全/私密请求，对应的 方法（method）, 标识（tag）, 版本（version）, 结构（structure） 都必须和 后端Request表中所指定的 一一对应，否则请求将不被通过。version 不传、为 null 或 <=0 都会使用最高版本，传了其它有效值则会使用最接近它的最低版本。下同。<br >\n4.GETS与GET、HEADS与HEAD分别为同一类型的操作方法，请求稍有不同但返回结果相同。下同。<br >\n5.在HTTP通信中，自动化接口(get,gets,head,heads,post,put,delete) 全用HTTP POST请求。下同。<br >\n6.所有JSONObject都视为容器(或者文件夹)，结构为 {...} ，里面可以放普通对象或子容器。下同。<br >\n7.每个对象都有一个唯一的路径(或者叫地址)，假设对象名为refKey，则用 key0/key1/.../refKey 表示。下同。\n\n<br >\n\n### <h3 id=\"3.2\">3.2 功能符<h3/>\n \n 功能 | 键值对格式 | 使用示例\n------------ | ------------ | ------------\n 查询数组 | \"key[]\":{}，后面是 JSONObject，key 可省略。当 key 和里面的 Table 名相同时，Table 会被提取出来，即 {Table:{Content}} 会被转化为 {Content} | [{\"User[]\":{\"User\":{}}}](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{}}})，查询一个 User 数组。这里 key 和 Table 名都是 User，User 会被提取出来，即 {\"User\":{\"id\", ...}} 会被转化为 {\"id\", ...}，如果要进一步提取 User 中的 id，可以把 User[] 改为 User-id[]，其中 - 用来分隔路径中涉及的 key\n 匹配选项范围 | \"key{}\":[]，后面是 JSONArray，作为 key 可取的值的选项 | [\"id{}\":[38710,82001,70793]](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"id{}\":[38710,82001,70793]}}})，对应 SQL 是`id IN(38710,82001,70793)`，查询 id 符合 38710,82001,70793 中任意一个的一个 User 数组\n 匹配条件范围 | \"key{}\":\"条件0,条件1...\"，条件为 SQL 表达式字符串，可进行数字比较运算等 | [\"id{}\":\"<=80000,\\>90000\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"id{}\":\"<=80000,\\>90000\"}}})，对应 SQL 是`id<=80000 OR id>90000`，查询 id 符合 id\\<=80000 \\| id>90000 的一个 User 数组\n 包含选项范围 | \"key<\\>\":value  =>  \"key<\\>\":[value]，key 对应值的类型必须为 JSONArray，value 值类型只能为 Boolean, Number, String 中的一种 |  [\"contactIdList<\\>\":38710](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"contactIdList<\\>\":38710}}})，对应SQL是`json_contains(contactIdList,38710)`，查询 contactIdList 包含 38710 的一个 User 数组\n 判断是否存在 | \"key}{@\":{<br /> &nbsp;&nbsp; \"from\":\"Table\",<br /> &nbsp;&nbsp; \"Table\":{ ... }<br />}<br />其中：<br />}{ 表示 EXISTS；<br /> key 用来标识是哪个判断；<br /> @ 后面是 子查询 对象，具体见下方 子查询 的说明。 | [\"id}{@\":{<br /> &nbsp;&nbsp; \"from\":\"Comment\",<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;  &nbsp;&nbsp; \"momentId\":15 <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"User\":{\"id}{@\":{\"from\":\"Comment\",\"Comment\":{\"momentId\":15}}}})<br /> WHERE EXISTS(SELECT * FROM Comment WHERE momentId=15)\n 远程调用函数 | \"key()\":\"函数表达式\"，函数表达式为 function(key0,key1...)，会调用后端对应的函数 function(JSONObject request, String key0, String key1...)，实现 参数校验、数值计算、数据同步、消息推送、字段拼接、结构变换 等特定的业务逻辑处理，<br>可使用 - 和 + 表示优先级，解析 key-() > 解析当前对象 > 解析 key() > 解析子对象 > 解析 key+()  | [\"isPraised()\":\"isContain(praiseUserIdList,userId)\"](http://apijson.cn:8080/get/{\"Moment\":{\"id\":301,\"isPraised()\":\"isContain(praiseUserIdList,userId)\"}})，会调用远程函数 [boolean isContain(JSONObject request, String array, String value)](https://github.com/APIJSON/apijson-framework/blob/master/src/main/java/apijson/framework/APIJSONFunctionParser.java#L361-L374) ，然后变为 \"isPraised\":true 这种（假设点赞用户id列表包含了userId，即这个User点了赞）\n 存储过程 | \"@key()\":\"SQL函数表达式\"，函数表达式为 <br />  function(key0,key1...) <br /> 会调用后端数据库对应的存储过程 SQL函数 <br /> function(String key0, String key1...) <br /> 除了参数会提前赋值，其它和 远程函数 一致 | [\"@limit\":10, <br /> \"@offset\":0, <br /> \"@procedure()\":\"getCommentByUserId(id,@limit,@offset)\"](http://apijson.cn:8080/get/{\"User\":{\"@limit\":10,\"@offset\":0,\"@procedure()\":\"getCommentByUserId(id,@limit,@offset)\"}}) <br /> 会转为 <br /> `getCommentByUserId(38710,10,0)` <br /> 来调用存储过程 SQL 函数 <br /> `getCommentByUserId(IN id bigint, IN limit int, IN offset int)` <br /> 然后变为 <br />\"procedure\":{<br /> &nbsp;&nbsp; \"count\":-1, <br /> &nbsp;&nbsp; \"update\":false, <br /> &nbsp;&nbsp; \"list\":[] <br /> } <br /> 其中 count 是指写操作影响记录行数，-1 表示不是写操作；update 是指是否为写操作（增删改）；list 为返回结果集\n 引用赋值 | \"key@\":\"key0/key1/.../refKey\"，引用路径为用/分隔的字符串。以/开头的是缺省引用路径，从声明key所处容器的父容器路径开始；其它是完整引用路径，从最外层开始。<br /> 被引用的refKey必须在声明key的上面。如果对refKey的容器指定了返回字段，则被引用的refKey必须写在@column对应的值内，例如 \"@column\":\"refKey,key1,...\" | [\"Moment\":{<br /> &nbsp;&nbsp; \"userId\":38710<br />},<br />\"User\":{<br /> &nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br />}](http://apijson.cn:8080/get/{\"Moment\":{\"userId\":38710},\"User\":{\"id@\":\"%252FMoment%252FuserId\"}})<br /> User内的id引用了与User同级的Moment内的userId，<br />即User.id = Moment.userId，请求完成后<br > \"id@\":\"/Moment/userId\" 会变成 \"id\":38710\n 子查询 | \"key@\":{<br /> &nbsp;&nbsp; \"range\":\"ALL\", <br /> &nbsp;&nbsp; \"from\":\"Table\",<br /> &nbsp;&nbsp; \"Table\":{ ... }<br />}<br />其中：<br />range 可为 ALL,ANY；<br />from 为目标表 Table 的名称；<br />@ 后面的对象类似数组对象，可使用 count 和 join 等功能。 | [\"id@\":{<br /> &nbsp;&nbsp; \"from\":\"Comment\",<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;  &nbsp;&nbsp; \"@column\":\"min(userId)\" <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"User\":{\"id@\":{\"from\":\"Comment\",\"Comment\":{\"@column\":\"min(userId)\"}}}})<br /> WHERE id=(SELECT min(userId) FROM Comment)\n 模糊搜索 | `\"key$\":\"SQL搜索表达式\"`  =>  `\"key$\":[\"SQL搜索表达式\"]`，任意SQL搜索表达式字符串，如 %key%(包含key), key%(以key开始), %k%e%y%(包含字母k,e,y) 等，%表示任意字符 | [\"name$\":\"%m%\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"name$\":\"%2525m%2525\"}}})，对应SQL是`name LIKE '%m%'`，查询name包含\"m\"的一个User数组\n 正则匹配 | \"key~\":\"正则表达式\"  =>  \"key~\":[\"正则表达式\"]，任意正则表达式字符串，如 ^[0-9]+$ ，*~ 忽略大小写，可用于高级搜索 | [\"name~\":\"^[0-9]+$\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"name~\":\"^[0-9]%252B$\"}}})，对应SQL是`name REGEXP '^[0-9]+$'`，查询name中字符全为数字的一个User数组\n 连续范围 | \"key%\":\"start,end\"  =>  \"key%\":[\"start,end\"]，其中 start 和 end 都只能为 Boolean, Number, String 中的一种，如 \"2017-01-01,2019-01-01\" ，[\"1,90000\", \"82001,100000\"] ，可用于连续范围内的筛选 | [\"date%\":\"2017-10-01,2018-10-01\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"date%2525\":\"2017-10-01,2018-10-01\"}}})，对应SQL是`date BETWEEN '2017-10-01' AND '2018-10-01'`，查询在2017-10-01和2018-10-01期间注册的用户的一个User数组\n 新建别名 | ① \"name:alias\"，name 映射为 alias，用 alias 替代 name，可用于 column,Table,SQL 函数 等，只用于 GET 类型、HEAD 类型的请求 <br /><br />② 函数调用映射<br />\"@key\": \"fun:avg(id);keyA:(keyB)\",<br />\"fun>\": 1,<br />\"keyA\": 1<br />其中 fun:fun(arg) 把 SQL 函数调用 fun(arg) 作为左侧表达式替代 fun，即 fun(arg) > 1；<br />keyA:(keyB) 表示将字段 keyA 重命名为 keyB，即实际 SQL 中为 keyB = 1，常用于重命名冲突的多条件同名字段。 | ① [\"@column\":\"toId:parentId\"](http://apijson.cn:8080/get/{\"Comment\":{\"@column\":\"id,toId:parentId\",\"id\":51}})，对应 SQL 是 `toId AS parentId`，将查询的字段 toId 变为 parentId 返回<br /><br />② [\"@key\": \"len:length(content);mid:(momentId)\",<br />\"len<=\": 10,<br />\"mid\": 12,<br />\"momentId\": 15,<br />\"@combine\": \"(len<= \\\\| mid) & momentId\"](http://apijson.cn/api?type=JSON&json={%22Comment%22:{%22@key%22:%22len%3Alength(content)%3Bmid%3A(momentId)%22,%22len%3C=%22:10,%22mid%22:12,%22momentId%22:15,%22@combine%22:%22(len%3C%3D%20%7C%20mid)%20%26%20momentId%22}})<br />对应 SQL 是 `(length(content) <= 10 OR momentId = 12) AND momentId = 15`\n 增加 或 扩展 | \"key+\":Object，Object的类型由key指定，且类型为Number,String,JSONArray中的一种。如 82001,\"apijson\",[\"url0\",\"url1\"] 等。只用于PUT请求 | \"praiseUserIdList+\":[82001]，对应SQL是`json_insert(praiseUserIdList,82001)`，添加一个点赞用户id，即这个用户点了赞\n 减少 或 去除 | \"key-\":Object，与\"key+\"相反 | \"balance-\":100.00，对应SQL是`balance = balance - 100.00`，余额减少100.00，即花费了100元\n 比较运算 | >, <, >=, <= 比较运算符，用于 <br />① 提供 \"id{}\":\"<=90000\" 这种条件范围的简化写法 <br /><br />② 实现子查询相关比较运算<br /><br />不支持 \"key=\":Object 和 \"key!=\":Object 这两种写法，直接用更简单的 \"key\":Object 和 \"key!\":Object 替代。 |  ① [\"id<=\":90000](http://apijson.cn:8080/get/{\"[]\":{\"User\":{\"id<=\":90000}}})，对应 SQL 是`id<=90000`，查询符合id<=90000的一个User数组<br /><br /> ② [\"id>@\":{<br /> &nbsp;&nbsp; \"from\":\"Comment\",<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;  &nbsp;&nbsp; \"@column\":\"min(userId)\" <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"User\":{\"id>@\":{\"from\":\"Comment\",\"Comment\":{\"@column\":\"min(userId)\"}}}})<br /> WHERE id>(SELECT min(userId) FROM Comment)\n 逻辑运算 | &, \\|, ! 逻辑运算符，对应数据库 SQL 中的 AND, OR, NOT。 <br />横或纵与：同一键值对的值内条件默认 \\| 或连接，可以在 key 后加逻辑运算符来具体指定；不同键值对的条件默认 & 与连接，可以用下面说明的对象关键词 @combine 来具体指定。 <br /><br />① & 可用于 \"key&{}\":\"条件\"等<br /><br />② \\| 可用于 \"key\\|{}\":\"条件\", \"key\\|{}\":[]等，一般可省略<br /><br />③ ! 可单独使用，如 \"key!\":Object，也可像 &,\\| 一样配合其他功能符使用 <br /> \"key!\":null 无效，null 值会导致整个键值对被忽略解析，可以用 \"key{}\":\"!=null\" 替代，<br />\"key\":null 同理，用 \"key{}\":\"=null\" 替代。 |  ① [\"id&{}\":\">80000,<=90000\"](http://apijson.cn:8080/head/{\"User\":{\"id&{}\":\">80000,<=90000\"}})，对应SQL是`id>80000 AND id<=90000`，即id满足id>80000 & id<=90000<br /><br /> ② [\"id\\|{}\":\">90000,<=80000\"](http://apijson.cn:8080/head/{\"User\":{\"id\\|{}\":\">90000,<=80000\"}})，同 \"id{}\":\">90000,<=80000\"，对应 SQL 是`id>90000 OR id<=80000`，即 id 满足 id>90000 \\| id<=80000<br /><br /> ③ [\"id!{}\":[82001,38710]](http://apijson.cn:8080/head/{\"User\":{\"id!{}\":[82001,38710]}})，对应 SQL 是`id NOT IN(82001,38710)`，即 id 满足 ! (id=82001 \\| id=38710)，可过滤黑名单的消息\n 数组关键词，可自定义 | \"key\":Object，key为 \"[]\":{} 中 {} 内的关键词，Object 的类型由 key 指定<br /><br />① \"count\":5，查询数量，0 表示最大值，默认值为 10，默认最大值为 100 <br /><br />② \"page\":1，查询页码，从 0 开始，默认值为 0，默认最大值为 100，一般和 count 一起用<br /><br />③ \"query\":2，查询内容<br />0-对象，1-总数和分页详情，2-数据、总数和分页详情<br />总数关键词为 total，分页详情关键词为 info，<br /> 它们都和 query 同级，通过引用赋值得到自定义 key:value 键值对，不传则返回默认键值对，例如  <br />\"total@\":\"/[]/total\", \"info@\":\"/[]/info\" <br />这里query及total仅为GET类型的请求提供方便，<br /> 一般可直接用HEAD类型的请求获取总数<br /><br />④ \"compat\": true <br />处理最外层查询字段有特殊处理的情况下 \"query\":2 返回查询总数不准确的问题：如DISTINCT去重、Aggregate 函数等<br /><br />⑤ \"join\":\"&/Table0,\\</Table1/key1@\" 或 <br />\"join\":{<br /> &nbsp;&nbsp; \"&/Table0\":{},  // 支持 ON 多个字段关联,<br /> &nbsp;&nbsp; \"\\</Table1/key1@\":{  // ON 只允许指定的 key1 关联<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"key0\":value0, // 其它ON条件 <br /> &nbsp;&nbsp;&nbsp;&nbsp; \"key2\":value2,<br /> &nbsp;&nbsp;&nbsp;&nbsp; ... <br /> &nbsp;&nbsp;&nbsp;&nbsp; \"@combine\":\"...\", // 其它ON条件的组合方式 <br /> &nbsp;&nbsp;&nbsp;&nbsp; \"@column\":\"...\", // 外层 SELECT <br /> &nbsp;&nbsp;&nbsp;&nbsp; \"@group\":\"...\", // 外层 GROUP BY <br /> &nbsp;&nbsp;&nbsp;&nbsp; \"@having\":\"...\" // 外层 HAVING <br /> &nbsp;&nbsp; }<br />}<br />多表连接方式：<br />  \"@\" - APP JOIN <br /> \"\\<\" - LEFT JOIN <br /> \">\" - RIGHT JOIN <br /> \"&\" - INNER JOIN <br /> \"\\|\" - FULL JOIN <br />  \"!\" - OUTER JOIN <br />  \"*\" - CROSS JOIN <br />  \"^\" - SIDE JOIN <br />  \"(\" - ANTI JOIN <br />  \")\" - FOREIGN JOIN <br />其中 @ APP JOIN 为应用层连表，会从已查出的主表里取得所有副表 key@ 关联的主表内的 refKey 作为一个数组 refKeys: [value0, value1...]，然后把原来副表 count 次查询 key=$refKey 的 SQL 用 key IN($refKeys) 的方式合并为一条 SQL 来优化性能；<br /> 其它 JOIN 都是 SQL JOIN，具体功能和 MySQL,PostgreSQL 等数据库的 JOIN 一一对应 <br />`\"join\":\"</ViceTable/key@\",`<br />`\"MainTable\":{},`<br />`\"ViceTable\":{\"key@\":\"/MainTable/refKey\"}`<br />会对应生成 <br />`MainTable LEFT JOIN ViceTable` <br />`ON ViceTable.key=MainTable.refKey` AND 其它ON条件 <br /> 除了 = 等价关联，也支持 ! 不等关联、\\> \\< \\>= \\<= 等比较关联和 $ ~ {} <> 等其它复杂关联方式 <br /><br />⑥ \"otherKey\":Object，自定义关键词，名称和以上系统关键词不一样，且原样返回上传的值  | ① 查询User数组，最多5个：<br />[\"count\":5](http://apijson.cn:8080/get/{\"[]\":{\"count\":5,\"User\":{}}})<br />对应 SQL 是`LIMIT 5` <br /><br /> ② 查询第3页的User数组，每页5个：<br />[\"count\":5,<br />\"page\":3](http://apijson.cn:8080/get/{\"[]\":{\"count\":5,\"page\":3,\"User\":{}}})<br />对应 SQL 是`LIMIT 5 OFFSET 15` <br /><br /> ③ 查询User数组和对应的User总数：<br />[\"[]\":{<br /> &nbsp;&nbsp; \"query\":2,<br /> &nbsp;&nbsp; \"User\":{}<br />},<br />\"total@\":\"/[]/total\", // 可省略<br />\"info@\":\"/[]/info\" // 可省略](http://apijson.cn:8080/get/{\"[]\":{\"query\":2,\"count\":5,\"User\":{}},\"total@\":\"%252F[]%252Ftotal\",\"info@\":\"%252F[]%252Finfo\"})<br /> 返回的数据中，总数及分页详情结构为： <br />  \"total\":139, // 总数 <br /> \"info\":{ // 分页详情 <br /> &nbsp;&nbsp; \"total\":139, // 总数 <br /> &nbsp;&nbsp; \"count\":5, // 每页数量 <br /> &nbsp;&nbsp; \"page\":0, // 当前页码 <br /> &nbsp;&nbsp; \"max\":27, // 最大页码 <br /> &nbsp;&nbsp; \"more\":true, // 是否还有更多 <br /> &nbsp;&nbsp; \"first\":true, // 是否为首页 <br /> &nbsp;&nbsp; \"last\":false // 是否为尾页 <br />} <br /><br /> ④查询User数组ID唯一情况下的User总数：<br />[\"[]\":{<br /> &nbsp;&nbsp; \"query\":2,<br /> &nbsp;&nbsp; \"compat\":\"true\",<br /> &nbsp;&nbsp; \"User\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"@column\":\"DISTINCT id\" <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"[]\":{\"query\":2,\"count\":5,\"User\":{}}})<br /> 返回的数据和结构同上 <br /><br /> ⑤ Moment INNER JOIN User LEFT JOIN Comment：<br />[\"[]\":{<br /> &nbsp;&nbsp; \"join\":\"&/User/id@,\\</Comment\",<br /> &nbsp;&nbsp; \"Moment\":{<br />&nbsp;&nbsp;&nbsp;&nbsp; \"@group\":\"id\" // 主副表不是一对一，要去除重复数据<br />&nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"User\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"name~\":\"t\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"/Moment/id\"<br /> &nbsp;&nbsp; }<br />}](http://apijson.cn/api/?type=JSON&url=http://apijson.cn:8080/get&json=%7B%22%5B%5D%22:%7B%22count%22:5,%22join%22:%22%26%2FUser%2Fid@,%3C%2FComment%22,%22Moment%22:%7B%22@column%22:%22id,userId,content%22,%22@group%22:%22id%22%7D,%22User%22:%7B%22name~%22:%22t%22,%22id@%22:%22%2FMoment%2FuserId%22,%22@column%22:%22id,name,head%22%7D,%22Comment%22:%7B%22momentId@%22:%22%2FMoment%2Fid%22,%22@column%22:%22id,momentId,content%22%7D%7D%7D)<br /><br /> ⑥ 每一层都加当前用户名：<br />[\"User\":{},<br />\"[]\":{<br /> &nbsp;&nbsp; \"name@\":\"User/name\", // 自定义关键词<br /> &nbsp;&nbsp; \"Moment\":{}<br />}](http://apijson.cn:8080/get/{\"User\":{},\"[]\":{\"name@\":\"User%252Fname\",\"Moment\":{}}})\n 对象关键词，可自定义 | \"@key\":Object，@key 为 Table:{} 中 {} 内的关键词，Object 的类型由 @key 指定<br /><br />① \"@combine\":\"key0 \\| (key1 & (key2 \\| !key3))...\"，条件组合方式，最终按 <br /> (其它key条件 AND 连接) AND (key0条件 OR (key1条件 AND (key2条件 OR (NOT key3条件)))) <br />这种方式连接，其中 \"其它key\" 是指与 @combine 在同一对象，且未被它声明的条件 key，默认都是 & 连接。注意不要缺少或多余任何一个空格。 <br /><br />② \"@column\":\"column;function(arg)...\"，返回字段<br /><br />③ \"@order\":\"column0+,column1-...\"，排序方式<br /><br />④ \"@group\":\"column0,column1...\"，分组方式。如果 @column 里声明了 Table 的 id，则 id 也必须在 @group 中声明；其它情况下必须满足至少一个条件:<br />1.分组的 key 在 @column 里声明<br />2.Table 主键在 @group 中声明 <br /><br />⑤ \"@having\":\"function0(...)?value0;function1(...)?value1;function2(...)?value2...\" // OR 连接，或 <br />\"@having&\":\"function0(...)?value0;function1(...)?value1;function2(...)?value2...\" // AND 连接，或 <br />\"@having\":{<br />&nbsp;&nbsp; \"h0\":\"function0(...)?value0\",<br />&nbsp;&nbsp; \"h1\":function1(...)?value1\",<br />&nbsp;&nbsp; \"h2\":function2(...)?value2...\",<br />&nbsp;&nbsp; \"@combine\":\"h0 & (h1 \\| !h2)\"  // 任意组合，非必传<br />}<br />SQL 函数条件，一般和 @group 一起用，函数一般在 @column 里声明<br /><br />⑥ \"@schema\":\"sys\"，集合空间(数据库名/模式)，非默认的值可通过它来指定，可以在最外层作为全局默认配置<br /><br />⑦ \"@database\":\"POSTGRESQL\"，数据库类型，非默认的值可通过它来指定，可以在最外层作为全局默认配置<br /><br />⑧ \"@datasource\":\"DRUID\"，跨数据源，非默认的值可通过它来指定，可以在最外层作为全局默认配置<br /><br />⑨ \"@json\":\"key0,key1...\"，转为 JSON 格式返回，符合 JSONObject 则转为 {...}，符合 JSONArray 则转为 \\[...] <br /><br />⑩ \"@role\":\"OWNER\"，来访角色，包括<br />UNKNOWN,LOGIN,CONTACT,CIRCLE,OWNER,ADMIN，<br />可以在最外层作为全局默认配置，<br />可自定义其它角色并重写 Verifier.verify 等相关方法来自定义校验 <br /><br />⑪ \"@explain\":true，性能分析，可以在最外层作为全局默认配置 <br /><br />⑫ \"@raw\":\"key0,key1...\"，其中 key0, key1 都对应有键值对<br /> \"key0\":\"SQL片段或SQL片段的别名\", <br /> \"key1\":\"SQL片段或SQL片段的别名\" <br /> 自定义原始SQL片段，可扩展嵌套SQL函数等复杂语句，必须是后端已配置的，只有其它功能符都做不到才考虑，谨慎使用，注意防 SQL 注入<br /><br />⑬ \"@null\":\"key1,key2...\"，空值键值对，自动插入 key1:null, key2:null ... 并作为有效键值对执行，作为条件时对应 SQL 是 `WHERE tag IS NULL`，作为值时对应 SQL 是 `SET tag = NULL`<br /><br />⑭ \"@otherKey\":Object，自定义关键词，名称和以上系统关键词不一样，且原样返回上传的值 | ① 搜索 name 或 tag 任何一个字段包含字符 a 的 User 列表：<br /> [\"name~\":\"a\",<br />\"tag~\":\"a\",<br />\"@combine\":\"name~ \\| tag~\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":10,\"User\":{\"@column\":\"id,name,tag\",\"name~\":\"a\",\"tag~\":\"a\",\"@combine\":\"name~%20%7C%20tag~\"}}}) <br />对应SQL是`name REGEXP 'a' OR tag REGEXP 'a'` <br /><br /> ② 只查询 id,sex,name 这几列并且请求结果也按照这个顺序：<br />[\"@column\":\"id,sex,name\"](http://apijson.cn:8080/get/{\"User\":{\"@column\":\"id,sex,name\",\"id\":38710}})<br />对应 SQL 是`SELECT id,sex,name` <br /><br /> ③ 查询按 name 降序、id 默认顺序 排序的 User 数组：<br />[\"@order\":\"name-,id\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"User\":{\"@column\":\"name,id\",\"@order\":\"name-,id\"}}})<br />对应 SQL 是`ORDER BY name DESC,id` <br /><br /> ④ 查询按 userId 分组的 Moment 数组：<br />[\"@group\":\"userId,id\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":%7B\"@column\":\"userId,id\",\"@group\":\"userId,id\"}}})<br />对应 SQL 是`GROUP BY userId,id` <br /><br /> ⑤ 查询 按 userId 分组、id 最大值>=100 的 Moment 数组：<br />[\"@column\":\"userId;max(id)\",<br />\"@group\":\"userId\",<br />\"@having\":\"max(id)>=100\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":{\"@column\":\"userId%253Bmax(id)\",\"@group\":\"userId\",\"@having\":\"max(id)>=100\"}}})<br />对应 SQL 是`SELECT userId,max(id) ... GROUP BY userId HAVING max(id)>=100` <br />还可以指定函数返回名：<br />[\"@column\":\"userId;max(id):maxId\",<br />\"@group\":\"userId\",<br />\"@having\":\"(maxId)>=100\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":{\"@column\":\"userId%253Bmax(id):maxId\",\"@group\":\"userId\",\"@having\":\"(maxId)>=100\"}}})<br />对应 SQL 是`SELECT userId,max(id) AS maxId ... GROUP BY userId HAVING (maxId)>=100` <br /><br /> ⑥ 查询 sys 内的 User 表：<br />[\"@schema\":\"sys\"](http://apijson.cn:8080/get/{\"User\":{\"@schema\":\"sys\"}})<br />对应 SQL 是`FROM sys.User` <br /><br /> ⑦ 查询 PostgreSQL 数据库的 User 表：<br />[\"@database\":\"POSTGRESQL\"](http://apijson.cn:8080/get/{\"User\":{\"@database\":\"POSTGRESQL\",\"@explain\":true}})<br /><br /> ⑧ 使用 Druid 连接池查询 User 表：<br />[\"@datasource\":\"DRUID\"](http://apijson.cn:8080/get/{\"User\":{\"@datasource\":\"DRUID\"}})<br /><br /> ⑨ 将 VARCHAR 字符串字段 get 转为 JSONArray 返回：<br />[\"@json\":\"get\"](http://apijson.cn:8080/get/{\"Access\":{\"@json\":\"get\"}})<br /><br /> ⑩ 查询当前用户的动态：<br />[\"@role\":\"OWNER\"](http://apijson.cn:8080/get/{\"[]\":{\"Moment\":{\"@role\":\"OWNER\"}}})<br /><br /> ⑪ 开启性能分析：<br />[\"@explain\":true](http://apijson.cn:8080/get/{\"[]\":{\"Moment\":{\"@explain\":true}}})<br />对应 SQL 是`EXPLAIN` <br /><br /> ⑫ 统计最近一周偶数 userId 的数量<br />[\"@column\":\"date;left(date,10):day;sum(if(userId%2=0,1,0))\",<br />\"@group\":\"day\",<br />\"@having\":\"to_days(now())-to_days(\\`date\\`)<=7\",<br />\"@raw\":\"@column,@having\"](http://apijson.cn:8080/get/{\"[]\":{\"Moment\":{\"@column\":\"date%3bleft(date,10):day%3bsum(if(userId%252=0,1,0))\",\"@group\":\"day\",\"@having\":\"to_days(now())-to_days(\\`date\\`)<=7\",\"@raw\":\"@column,@having\"}}})<br />对应 SQL 是``SELECT date, left(date,10) AS day, sum(if(userId%2=0,1,0)) ... GROUP BY day HAVING to_days(now())-to_days(`date`)<=7`` <br /><br /> ⑬ 把用户的标签设置为空 <br />[\"@null\":\"tag\"](http://apijson.cn/api/?type=JSON&url=http://apijson.cn:8080/put/User&json={%22id%22:82001,%22@null%22:%22tag%22,%22@explain%22:true}) <br /><br /> ⑭  从pictureList 获取第 0 张图片：<br />[\"@position\":0, // 自定义关键词<br />\"firstPicture()\":\"getFromArray(pictureList,@position)\"](http://apijson.cn:8080/get/{\"User\":{\"id\":38710,\"@position\":0,\"firstPicture()\":\"getFromArray(pictureList,@position)\"}})\n 全局关键词 | 为最外层对象 {} 内的关键词。其中 @database，@schema, @datasource, @role, @explain 基本同对象关键词，见上方说明，区别是全局关键词会每个表对象中没有时自动放入，作为默认值。 <br /><br />① \"tag\":\"Table\"，后面的 tag 是非 GET、HEAD 请求中匹配请求的 JSON 结构的标识，一般是要查询的 Table 的名称或该名称对应的数组 Table[] 或 Table:[]，由后端 Request 表中指定。 <br /><br />② \"version\":1，接口版本，version 不传、为 null 或 <=0 都会使用最高版本，传了其它有效值则会使用最接近它的最低版本，由后端 Request 表中指定。<br /><br />③ \"format\":true，格式化返回 Response JSON 的 key，一般是将 TableName 转为 tableName, TableName[] 转为 tableNameList, Table:alias 转为 alias, TableName-key[] 转为 tableNameKeyList 等小驼峰格式。  | ①  查隐私信息：<br />[{\"tag\":\"Privacy\",\"Privacy\":{\"id\":82001}}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={%22tag%22:%22Privacy%22,%22Privacy%22:{%22id%22:82001}})<br /><br /> ② 使用第 1 版接口查隐私信息：<br />[{\"version\":1,\"tag\":\"Privacy\",\"Privacy\":{\"id\":82001}}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={%22version%22:1,%22tag%22:%22Privacy%22,%22Privacy%22:{%22id%22:82001}}) <br /><br /> ③ 格式化朋友圈接口返回 JSON 中的 key：<br />[{<br > &nbsp;&nbsp; \"format\":true, <br > &nbsp;&nbsp; \"[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{}, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"count\":3,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"[]/Moment/id\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"format\":true,\"[]\":{\"page\":0,\"count\":3,\"Moment\":{},\"User\":{\"id@\":\"%252FMoment%252FuserId\"},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n<br />\n"
  },
  {
    "path": "Document.md",
    "content": "## English | [中文](https://github.com/Tencent/APIJSON/blob/master/Document-Chinese.md)\n\n#### A better online document is available at https://apijsondocs.readthedocs.io\n\n### Examples:\n\n#### Get a User\nRequest:\n<pre><code class=\"language-json\">{\n  \"User\":{\n  }\n}\n</code></pre>\n\n[Click here to test](http://apijson.cn:8080/get/{\"User\":{}})\n\nResponse:\n<pre><code class=\"language-json\">{\n  \"User\":{\n    \"id\":38710,\n    \"sex\":0,\n    \"name\":\"TommyLemon\",\n    \"certified\":true,\n    \"tag\":\"Android&Java\",\n    \"phone\":13000038710,\n    \"head\":\"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n    \"date\":1485948110000,\n    \"pictureList\":[\n      \"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n      \"http://common.cnblogs.com/images/icon_weibo_24.png\"\n    ]\n  },\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<br />\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON single objects: simple queries, statistics, groups, orders, aggregations, comparisons, filters, aliases, etc.</a>\n</p> \n  \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_single.gif)\n\n#### Get an array of Users\n\nRequest:\n<pre><code class=\"language-json\">{\n  \"[]\":{\n    \"count\":3, //just get 3 results\n    \"User\":{\n      \"@column\":\"id,name\" //just get ids and names\n    }\n  }\n}\n</code></pre>\n\n[Click here to test](http://apijson.cn:8080/get/{\"[]\":{\"count\":3,\"User\":{\"@column\":\"id,name\"}}})\n\nResponse:\n<pre><code class=\"language-json\">{\n  \"[]\":[\n    {\n      \"User\":{\n        \"id\":38710,\n        \"name\":\"TommyLemon\"\n      }\n    },\n    {\n      \"User\":{\n        \"id\":70793,\n        \"name\":\"Strong\"\n      }\n    },\n    {\n      \"User\":{\n        \"id\":82001,\n        \"name\":\"Android\"\n      }\n    }\n  ],\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<br />\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON single arrays: simple queries, statistics, groups, orders, aggregations, paginations, searches, regexps, combinations, etc.</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_array.gif)\n\n\n#### Get a Moment and its publisher\nRequest:\n<pre><code class=\"language-json\">{\n  \"Moment\":{\n  },\n  \"User\":{\n    \"id@\":\"Moment/userId\"  //User.id = Moment.userId\n  }\n}\n</code></pre>\n\n[Click here to test](http://apijson.cn:8080/get/{\"Moment\":{},\"User\":{\"id@\":\"Moment%252FuserId\"}})\n\nResponse:\n<pre><code class=\"language-json\">{\n  \"Moment\":{\n    \"id\":12,\n    \"userId\":70793,\n    \"date\":\"2017-02-08 16:06:11.0\",\n    \"content\":\"1111534034\"\n  },\n  \"User\":{\n    \"id\":70793,\n    \"sex\":0,\n    \"name\":\"Strong\",\n    \"tag\":\"djdj\",\n    \"head\":\"http://static.oschina.net/uploads/user/585/1170143_50.jpg?t=1390226446000\",\n    \"contactIdList\":[\n      38710,\n      82002\n    ],\n    \"date\":\"2017-02-01 19:21:50.0\"\n  },\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n  \n<br />\n\n#### Get a Moment list like Twitter tweets\nRequest:\n<pre><code class=\"language-json\">{\n  \"[]\":{                             //get an array\n    \"page\":0,                        //pagination\n    \"count\":2,\n    \"Moment\":{                       //get a Moment\n      \"content$\":\"%a%\"               //filter condition: content contains 'a'\n    },\n    \"User\":{\n      \"id@\":\"/Moment/userId\",        //User.id = Moment.userId, short reference path，starts from grandparents path\n      \"@column\":\"id,name,head\"       //get specified keys with the written order \n    },\n    \"Comment[]\":{                    //get a Comment array, and unwrap Comment object\n      \"count\":2,\n      \"Comment\":{\n        \"momentId@\":\"[]/Moment/id\"   //Comment.momentId = Moment.id, full reference path\n      }\n    }\n  }\n}\n</code></pre>\n\n[Click here to test](http://apijson.cn:8080/get/{\"[]\":{\"page\":0,\"count\":2,\"Moment\":{\"content$\":\"%2525a%2525\"},\"User\":{\"id@\":\"%252FMoment%252FuserId\",\"@column\":\"id,name,head\"},\"Comment[]\":{\"count\":2,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n\nResponse:\n<pre><code class=\"language-json\">{\n  \"[]\":[\n    {\n      \"Moment\":{\n        \"id\":15,\n        \"userId\":70793,\n        \"date\":1486541171000,\n        \"content\":\"APIJSON is a JSON Transmission Protocol…\",\n        \"praiseUserIdList\":[\n          82055,\n          82002,\n          82001\n        ],\n        \"pictureList\":[\n          \"http://static.oschina.net/uploads/user/1218/2437072_100.jpg?t=1461076033000\",\n          \"http://common.cnblogs.com/images/icon_weibo_24.png\"\n        ]\n      },\n      \"User\":{\n        \"id\":70793,\n        \"name\":\"Strong\",\n        \"head\":\"http://static.oschina.net/uploads/user/585/1170143_50.jpg?t=1390226446000\"\n      },\n      \"Comment[]\":[\n        {\n          \"id\":176,\n          \"toId\":166,\n          \"userId\":38710,\n          \"momentId\":15,\n          \"date\":1490444883000,\n          \"content\":\"thank you\"\n        },\n        {\n          \"id\":1490863469638,\n          \"toId\":0,\n          \"userId\":82002,\n          \"momentId\":15,\n          \"date\":1490863469000,\n          \"content\":\"Just do it\"\n        }\n      ]\n    },\n    {\n      \"Moment\":{\n        \"id\":58,\n        \"userId\":90814,\n        \"date\":1485947671000,\n        \"content\":\"This is a Content...-435\",\n        \"praiseUserIdList\":[\n          38710,\n          82003,\n          82005,\n          93793,\n          82006,\n          82044,\n          82001\n        ],\n        \"pictureList\":[\n          \"http://static.oschina.net/uploads/img/201604/22172507_aMmH.jpg\"\n        ]\n      },\n      \"User\":{\n        \"id\":90814,\n        \"name\":7,\n        \"head\":\"http://static.oschina.net/uploads/user/51/102723_50.jpg?t=1449212504000\"\n      },\n      \"Comment[]\":[\n        {\n          \"id\":13,\n          \"toId\":0,\n          \"userId\":82005,\n          \"momentId\":58,\n          \"date\":1485948050000,\n          \"content\":\"This is a Content...-13\"\n        },\n        {\n          \"id\":77,\n          \"toId\":13,\n          \"userId\":93793,\n          \"momentId\":58,\n          \"date\":1485948050000,\n          \"content\":\"This is a Content...-77\"\n        }\n      ]\n    }\n  ],\n  \"code\":200,\n  \"msg\":\"success\"\n}\n</code></pre>\n\n<p align=\"center\" >\n  <a >[GIF] APIJSON query multi related tables: one to one, one to many, many to one, various conditions, etc.</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_associate.gif)\n\n<br />\n  \n<p align=\"center\" >\n  <a >[GIF] APIJSON joins: < LEFT JOIN, & INNER JOIN, etc.</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_join.gif)\n  \n<br />\n  \n<p align=\"center\" >\n  <a >[GIF] APIJSON subqueries：@from@ FROM, key@ =, key>@ >, key{}@ IN, key}{@ EXISTS, etc.</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_subquery.gif)\n    \n<br />\n    \n<p align=\"center\" >\n  <a >[GIF] APIJSON: a set of some features, simple to complex</a>\n</p> \n\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_summary.gif)\n\n<br />\n\n[Test it online](http://apijson.cn/api) \n\n<br />\n<br />\n\n## API Design Rules\n\n### 1. Methods and API endpoints\n\n Methods | URL | Request | Response\n------------ | ------------ | ------------ | ------------\n**GET**: <br /> A general way to get data.<br /> You can use dev tools to make edits in a web browser. | base_url/get/ | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; //Add contiditions here.<br /> &nbsp;&nbsp; }<br />} <br /> <br /> Eg. To get a Moment with `id = 235`：<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235<br /> &nbsp;&nbsp; }<br />} | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; ...<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br />}<br />Eg.<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br /> }\n**HEAD**: <br /> A general way to get counts.<br /> You can use dev tools to make edits in a web browser. | base_url/head/ | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; …<br /> &nbsp;&nbsp; }<br /> } <br /> {…} are conditions. <br /><br /> Eg. Get the number of Moments posted by the user with `id = 38710`：<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710<br /> &nbsp;&nbsp; }<br />} | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"count\":10<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br />} <br /> Eg.<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"count\":10<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;  \"msg\":\"success\"<br />}\n**GETS**: <br /> Get data with high security and confidentiality.<br /> Eg. bank accounts, birth date. | base_url/gets/ | You need to add `\"tag\":tag` with the same level of `Moment:{}`. Others are the same as **GET**. | Same as **GET**.\n**HEADS**: <br /> Get counts of confidential data(eg. bank account).| base_url/heads/ | You need to add  `\"tag\":tag` with the same level of `Moment:{}`. Others are the same as **HEAD**. | Same as **HEAD**.\n**POST**: <br /> Add new data. | base_url/post/ | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; …<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":tag<br />} <br /> The id in {...} is generated automatically when table is built and can’t be set by the user. <br /><br />Eg. A user with `id = 38710` posts a new Moment：<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"userId\":38710,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":\"Moment\"<br />} | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":38710<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br />}<br />Eg.<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":120<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br />}\n**PUT**: <br /> Make changes to a specific item.<br /> Only change the part sent to server. | base_url/put/ | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":id,<br /> &nbsp;&nbsp;&nbsp;&nbsp; …<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":tag<br />} <br />  You can also add multiple id as `id{}`.<br /><br />Eg. Make changes to Moment's content with id= 235:<br />{<br /> &nbsp;&nbsp; \"Moment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":235,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"content\":\"APIJSON is the real-time coding-free, powerful and secure ORM\"<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":\"Moment\"<br />} | Same as **POST**.\n**DELETE**: <br /> Delete data. | base_url/delete/ | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id\":id<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":tag<br />} <br /> You can also add multiple id as `id{}`. <br /><br /> Or Delete contents with multiple id：<br />{<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id{}\":[100,110,120]<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"tag\":\"Comment[]\"<br />} | {<br /> &nbsp;&nbsp; TableName:{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"msg\":\"success\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id[]\":[100,110,120]<br />&nbsp;&nbsp; &nbsp;&nbsp; \"count\":3<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"code\":200,<br /> &nbsp;&nbsp; \"msg\":\"success\"<br />}<br />Eg.<br />{<br />&nbsp;&nbsp; \"Comment\":{<br />&nbsp;&nbsp; &nbsp;&nbsp; \"code\":200,<br />&nbsp;&nbsp; &nbsp;&nbsp; \"msg\":\"success\",<br />&nbsp;&nbsp; &nbsp;&nbsp; \"id[]\":[100,110,120],<br />&nbsp;&nbsp; &nbsp;&nbsp; \"count\":3<br />&nbsp;&nbsp; },<br />&nbsp;&nbsp; \"code\":200,<br />&nbsp;&nbsp; \"msg\":\"success\"<br />}\n\n**Note**:<br />\n1. TableName means the name of the table where you get data. It’ll respond with a JSON Object(the form is {....})with columns inside.\n2. `\"tag\":tag` is needed when methods are not GET or HEAD. The tag after the colon is the key in JSON Object of making requests. Generally, it’s the name of the table you’re looking for.\n3. GET, HEAD are methods for general data requests.They support versatile JSON Object structure. Other methods are used for requesting confidential data and the requesting JSON Object needs to be in the same form/order as that in the database. Otherwise, the request shall be denied.\n4. GETS and GET, HEADS and HEAD return the same type of data. But the request form is a little different.\n5. For HTTP, all API methods (get,gets,head,heads,post,put,delete) make requests with HTTP POST.\n6. All JSON Objects here are with {...} form. You can put items or objects in it.\n7. Each object in the database has a unique address.\n\n<br />\n\n### 2. Keyswords in URL parameters\n \n Functions | Key-value pairs | Examples\n------------ | ------------ | ------------\n Get data in arrays | `\"key[]\":{}`<br /> The part after the colon is a JSONObject. *key* is optional. When *key* is the same as the table name , the JSONObject will be in a simplified form. For example,  `{Table:{Content}}` will be written as `{Content}`.| [{\"User[]\":{\"User\":{}}}](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{}}}) <br />It is used for getting data from a user. Here, key and tablename are all \"User\", then <br />`{\"User\":{\"id\", ...}}` <br />will be written as <br /> `{\"id\", ...}`\n Get data that meets specific conditions | `\"key{}\":[]` <br />The part after the colon is a JSONArray with conditions inside.| [\"id{}\":[38710,82001,70793]](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"id{}\":[38710,82001,70793]}}}) <br />In SQL, this would be `id IN(38710,82001,70793)`. <br />It means getting data with id equals 38710,82001,70793.\n Get data with comparison operation| `\"key{}\":\"condition0,condition1...\"`<br />Conditions can be any SQL comparision operation. Use''to include any non-number characters.| [\"id{}\":\"<=80000,\\>90000\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"id{}\":\"<=80000,\\>90000\"}}}) <br />In SQL, it'd be <br /> `id<=80000 OR id>90000`, <br />which means get User array with id\\<=80000 \\| id>90000\n Get data that contains an element | `\"key<>\":Object`  =>  `\"key<>\":[Object]` <br /> *key* must be a JSONArray while *Object* cannot be JSON.|  [\"contactIdList<\\>\":38710](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"contactIdList<\\>\":38710}}}) <br />In SQL, this would be <br />`json_contains(contactIdList,38710)`. <br />It means find data of the User whose contactList contains 38710.\n See if it exists |`\"key}{@\":{`<br /> &nbsp;&nbsp; `\"from\":\"Table\",`<br /> &nbsp;&nbsp; `\"Table\":{ ... }`<br />`}`<br /><br />}{ means EXISTS.<br /> *key* is the one you want to check. <br />Here is a *Subquery* in it, see specifications below for more information. | [\"id}{@\":{<br /> &nbsp;&nbsp; \"from\":\"Comment\",<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;  &nbsp;&nbsp; \"momentId\":15 <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"User\":{\"id}{@\":{\"from\":\"Comment\",\"Comment\":{\"momentId\":15}}}})<br /> WHERE EXISTS(SELECT * FROM Comment WHERE momentId=15)\n Include functions in parameters | `\"key()\":\"function (key0,key1...)\"`<br />This will trigger the back-end <br /> `function(JSONObject request, String key0, String key1...)` <br />to get or testify data.<br /> Use - and + to show the order of priority: analyze key-() > analyze the current object > analyze key() > analyze child object > analyze key+()| [\"isPraised()\":\"isContain(praiseUserIdList,userId)\"](http://apijson.cn:8080/get/{\"Moment\":{\"id\":301,\"isPraised()\":\"isContain(praiseUserIdList,userId)\"}}) <br />This will use function boolean isContain(JSONObject request, String array, String value). In this case, client will get \"isPraised\":true(In this case, client use function to testify if a user clicked ‘like’ button for a Moment.)\n Refer a value | `\"key@\":\"key0/key1/.../refKey\"`<br />Use / to show path. The part before the colon is the key that wants to refer. The path after the colon starts with the parent level of the key.| [\"Moment\":{<br /> &nbsp;&nbsp; \"userId\":38710<br />},<br />\"User\":{<br /> &nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br />}](http://apijson.cn:8080/get/{\"Moment\":{\"userId\":38710},\"User\":{\"id@\":\"%252FMoment%252FuserId\"}})<br /> In this example, the value of id in User refer to the *userId* in *Moment*, which means <br />`User.id = Moment.userId`. <br />After the request is sent, <br />`\"id@\":\"/Moment/userId\"` will be `\"id\":38710`.\n Subquery | `\"key@\":{`<br /> &nbsp;&nbsp; `\"range\":\"ALL\",` <br /> &nbsp;&nbsp; `\"from\":\"Table\",`<br /> &nbsp;&nbsp; `\"Table\":{ ... }`<br />`}`<br />*range* can be ALL, ANY.<br />*from* means which table you want to query. <br />It’s very similar to how you query in SQL. <br />You can also use *count*, *join*, etc. | [\"id@\":{<br /> &nbsp;&nbsp; \"from\":\"Comment\",<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;  &nbsp;&nbsp; \"@column\":\"min(userId)\" <br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"User\":{\"id@\":{\"from\":\"Comment\",\"Comment\":{\"@column\":\"min(userId)\"}}}})<br /> `WHERE id=(SELECT min(userId) FROM Comment)`.\n Fuzzy matching | `\"key$\":\"SQL search expressions\"`  =>  `\"key$\":[\"SQL search expressions\"]`<br />Any SQL search expressions.Eg.%key%(include key), key%(start with key),%k%e%y%(include k, e, y). % means any characters. | [\"name$\":\"%m%\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"name$\":\"%2525m%2525\"}}}) <br />In SQL, it's <br />`name LIKE '%m%'`, <br />meaning that get User with ‘m’ in name.\n Regular Expression| `\"key~\":\"regular expression\"`  =>  `\"key~\":[\"regular expression\"]`<br />It can be any regular expressions.Eg. ^[0-9]+$ ，*~ not case sensitive, advanced search is applicable.| [\"name~\":\"^[0-9]+$\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"name~\":\"^[0-9]%252B$\"}}}) <br />In SQL, it's <br />`name REGEXP '^[0-9]+$'`.\n Get data in a range| `\"key%\":\"start,end\"`  =>  `\"key%\":[\"start,end\"]`<br />The data type of start and end can only be either Boolean, Number or String. Eg. \"2017-01-01,2019-01-01\" ，[\"1,90000\", \"82001,100000\"]. It's used for getting data from a specific time range. | [\"date%\":\"2017-10-01,2018-10-01\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":3,\"User\":{\"date%2525\":\"2017-10-01,2018-10-01\"}}}) <br />In SQL, it's <br />`date BETWEEN '2017-10-01' AND '2018-10-01'`, <br />meaning to get User data that registered between 2017-10-01 and 2018-10-01.\n Make an alias | `\"name:alias\"`<br />this changes name to alias in returning results. It’s applicable to column, tableName, SQL Functions, etc. but only in GET, HEAD requests. | [\"@column\":\"toId:parentId\"](http://apijson.cn:8080/get/{\"Comment\":{\"@column\":\"id,toId:parentId\",\"id\":51}}) <br />In SQL, it's <br />`toId AS parentId`. <br />It'll return `parentId` instead of `toId`.<br /><br />For @key format like \"lc_wai6b3vk2:(lc_wai6b3vk)\", it means renaming field lc_wai6b3vk2 to lc_wai6b3vk, commonly used for field renaming scenarios. Example:<br />{<br />&nbsp;&nbsp;\"lc_sinan_ba074fbb\": {<br />&nbsp;&nbsp;&nbsp;&nbsp;\"lc_wai6b3vk\": \"11\",<br />&nbsp;&nbsp;&nbsp;&nbsp;\"lc_wai6b3vk2\": \"22\",<br />&nbsp;&nbsp;&nbsp;&nbsp;\"@combine\": \"lc_wai6b3vk \\\\| lc_wai6b3vk2\",<br />&nbsp;&nbsp;&nbsp;&nbsp;\"@key\": \"lc_wai6b3vk2:(lc_wai6b3vk)\"<br />&nbsp;&nbsp;}<br />}<br />corresponds to SQL `(lc_wai6b3vk = '11' OR lc_wai6b3vk2 = '22')`, but the lc_wai6b3vk2 field will be renamed and displayed as lc_wai6b3vk in the returned result\n Add / expand an item | `\"key+\":Object` <br /> The type of Object is decided by *key*. Types can be Number, String, JSONArray. Froms are 82001,\"apijson\",[\"url0\",\"url1\"] respectively. It’s only applicable to PUT request.| \"praiseUserIdList+\":[82001]. In SQL, it's <br />`json_insert(praiseUserIdList,82001)`. <br />Add an *id* that praised the Moment.\n Delete / decrease an item | `\"Key-\":Object`<br /> It’s the contrary of \"key+\" | \"balance-\":100.00. In SQL, it's <br />`balance = balance - 100.00`, <br />meaning there's 100 less in balance.\n Operations | &, \\|, ! <br /> They're used in logic operations. It’s the same as AND, OR, NOT in SQL respectively. <br />By default, for the same key, it’s ‘\\|’ (OR)operation among conditions; for different keys, the default operation among conditions is ‘&’(AND). <br /> |  ① [\"id&{}\":\">80000,<=90000\"](http://apijson.cn:8080/head/{\"User\":{\"id&{}\":\">80000,<=90000\"}}) <br />In SQL, it's <br />`id>80000 AND id<=90000`, <br />meaning *id* needs to be id>80000 & id<=90000<br /><br /> ② [\"id\\|{}\":\">90000,<=80000\"](http://apijson.cn:8080/head/{\"User\":{\"id\\|{}\":\">90000,<=80000\"}}) <br />It's the same as \"id{}\":\">90000,<=80000\". <br />In SQL, it's <br />`id>80000 OR id<=90000`, <br />meaning that *id* needs to be id>90000 \\| id<=80000<br /><br /> ③ [\"id!{}\":[82001,38710]](http://apijson.cn:8080/head/{\"User\":{\"id!{}\":[82001,38710]}}) <br />In SQL, it's <br />`id NOT IN(82001,38710)`, <br />meaning id needs to be ! (id=82001 \\| id=38710).\n Keywords in an Array: It can be self-defined. | As for `\"key\":Object`, *key* is the keyword of *{}* in *\"[]\":{}*. The type of *Object* is up to *key*.<br /><br />① `\"count\":Integer` It's used to count the number. The default largest number is 100. <br /><br />② `\"page\":Integer` It’s used for getting data from which page, starting from 0. The default largest number is 100. It’s usually used with COUNT. <br /><br />③ `\"query\":Integer` Get the number of items that match conditions<br />When to get the object, the integer should be 0; when to get the total number, it’s 1; when both above, it’s 2.<br />You can get the total number with keyword total. It can be referred to other values. <br />Eg. <br />`\"total@\":\"/[]/total\"` <br />Put it as the same level of query.  <br />*Query* and *total* are used in GET requests just for convenience. Generally, HEAD request is for getting numbers like the total number.<br /><br />④ `\"join\":\"&/Table0,</Table1\"`<br />Join tables：<br /> \"\\<\" - LEFT JOIN <br /> \">\" - RIGHT JOIN <br /> \"&\" - INNER JOIN <br /> \"\\|\" - FULL JOIN <br />  \"!\" - OUTER JOIN <br /> \"@\" - APP JOIN <br />Where @ APP JOIN is in application layer.It’ll get all the keys in tables that refKeys in result tables are referred to, like refKeys:[value0, value1….]. Then, as the results get data according to `key=$refKey` a number of times (COUNT), it uses key `IN($refKeys)` to put these counts together in just one SQL query, in order to improve the performance.<br /> Other JOIN functions are the same as those in SQL. <br />`\"join\":\"</ViceTable\",`<br />`\"MainTable\":{},`<br />`\"ViceTable\":{\"key@\":\"/MainTable/refKey\"}`<br />will return <br />`MainTable LEFT JOIN ViceTable` <br />`ON ViceTable.key=MainTable.refKey` <br /><br />⑤ `\"otherKey\":Object` Self-defined keyword other than those that already in the system. It also returns with self-defined keywords.| ① Get User arrays with maximum of 5：<br />[\"count\":5](http://apijson.cn:8080/get/{\"[]\":{\"count\":5,\"User\":{}}})<br /><br /> ② Look into User arrays on page 3. Show 5 of them each page. <br />[\"count\":5,<br />\"page\":3](http://apijson.cn:8080/get/{\"[]\":{\"count\":5,\"page\":3,\"User\":{}}})<br /><br /> ③ Get User Arrays and count the total number of Users：<br />[\"[]\":{<br /> &nbsp;&nbsp; \"query\":2,<br /> &nbsp;&nbsp; \"User\":{}<br />},<br />\"total@\":\"/[]/total\"](http://apijson.cn:8080/get/{\"[]\":{\"query\":2,\"count\":5,\"User\":{}},\"total@\":\"%252F[]%252Ftotal\"})<br />Questions like total page numbers or if there's next page can be solved by total,count,page functions，<br />Total page number: <br/>`int totalPage = Math.ceil(total / count)`<br />If this is the last page: <br />`boolean hasNextPage = total > count*page`<br />If this is the first page: <br />`boolean isFirstPage = page <= 0`<br />If it's the last page: <br />`boolean isLastPage = total <= count*page`<br />... <br /><br /> ④ Moment INNER JOIN User LEFT JOIN Comment：<br />[\"[]\":{<br /> &nbsp;&nbsp; \"join\": \"&/User,\\</Comment\",<br /> &nbsp;&nbsp; \"Moment\":{},<br /> &nbsp;&nbsp; \"User\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"name~\":\"t\",<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"id@\": \"/Moment/userId\"<br /> &nbsp;&nbsp; },<br /> &nbsp;&nbsp; \"Comment\":{<br /> &nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\": \"/Moment/id\"<br /> &nbsp;&nbsp; }<br />}](http://apijson.cn:8080/get/{\"[]\":{\"count\":5,\"join\":\"&%252FUser,\\<%252FComment\",\"Moment\":{\"@column\":\"id,userId,content\"},\"User\":{\"name~\":\"t\",\"id@\":\"%252FMoment%252FuserId\",\"@column\":\"id,name,head\"},\"Comment\":{\"momentId@\":\"%252FMoment%252Fid\",\"@column\":\"id,momentId,content\"}}})<br /><br /> ⑤ Add the current user to every level：<br />[\"User\":{},<br />\"[]\":{<br /> &nbsp;&nbsp; \"name@\":\"User/name\", //self-defined keyword<br /> &nbsp;&nbsp; \"Moment\":{}<br />}](http://apijson.cn:8080/get/{\"User\":{},\"[]\":{\"name@\":\"User%252Fname\",\"Moment\":{}}})\n Keywords in Objects: It can be self-defined. | `\"@key\":Object` @key is the keyword of {} in Table:{}. The type of Object is decided by @key<br /><br />① `\"@combine\":\"&key0,&key1,\\|key2,key3,`<br />`!key4,!key5,&key6,key7...\"`<br />First, it’ll group data with same operators. Within one group, it operates from left to right. Then it’ll follow the order of & \\| ! to do the operation. Different groups are connected with &. So the expression above will be : <br /> (key0 & key1 & key6 & other key) & (key2 \\| key3 \\| key7) & !(key4 \\| key5) <br />\\| is optional. <br /><br />② `\"@column\":\"column;function(arg)...\"` Return with specific columns.<br /><br />③ `\"@order\":\"column0+,column1-...\"` Decide the order of returning results:<br /><br />④ `\"@group\":\"column0,column1...\"` How to group data. If @column has declared Table id, this id need to be included in @group. In other situations, at least one of the following needs to be done:<br />1.Group id is declared in @column<br />2.Primary Key of the table is declared in @group.<br/><br/>⑤ `@having\":\"function0(...)?value0;function1(...)?value1;function2(...)?value2...\"` Add conditions on return results with @having. Usually working with@group, it’s declared in @column.<br /><br />⑥ `\"@schema\":\"sys\"` Can be set as default setting.<br /><br />⑦ `\"@database\":\"POSTGRESQL\"` Get data from a different database.Can be set as default setting.<br /><br />⑧ `\"@role\":\"OWNER\"` Get information of the user, including <br />UNKNOWN,LOGIN,CONTACT,CIRCLE,OWNER,ADMIN，<br />Can be set as default setting. <br />You can self-define a new role or rewrite a role.  Use`Verifier.verify` etc. to self-define validation methods. <br /><br />⑨ `\"@explain\":true` Profiling. Can be set as default setting. <br /><br />⑩ `\"@otherKey\":Object` Self-define keyword | ① Search *Users* that *name* or *tag* contains the letter \"a\":<br />[\"name~\":\"a\",<br />\"tag~\":\"a\",<br />\"@combine\":\"name~,tag~\"](http://apijson.cn:8080/get/{\"User[]\":{\"count\":10,\"User\":{\"@column\":\"id,name,tag\",\"name~\":\"a\",\"tag~\":\"a\",\"@combine\":\"name~,tag~\"}}})<br /><br /> ② Only search column id,sex,name and return with the same order:<br />[\"@column\":\"id,sex,name\"](http://apijson.cn:8080/get/{\"User\":{\"@column\":\"id,sex,name\",\"id\":38710}})<br /><br /> ③ Search Users that have descending order of name and default order of id:<br />[\"@order\":\"name-,id\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"User\":{\"@column\":\"name,id\",\"@order\":\"name-,id\"}}})<br /><br /> ④ Search Moment grouped with userId: <br />[\"@group\":\"userId,id\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":%7B\"@column\":\"userId,id\",\"@group\":\"userId,id\"}}})<br /><br /> ⑤ Search Moments that id equals or less than 100 and group with userId:<br />[\"@column\":\"userId;max(id)\",<br />\"@group\":\"userId\",<br />\"@having\":\"max(id)>=100\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":{\"@column\":\"userId%253Bmax(id)\",\"@group\":\"userId\",\"@having\":\"max(id)>=100\"}}})<br />You can also define the name of the returned function:<br />[\"@column\":\"userId;max(id):maxId\",<br />\"@group\":\"userId\",<br />\"@having\":\"(maxId)>=100\"](http://apijson.cn:8080/get/{\"[]\":{\"count\":10,\"Moment\":{\"@column\":\"userId%253Bmax(id):maxId\",\"@group\":\"userId\",\"@having\":\"(maxId)>=100\"}}})<br /><br /> ⑥ Check Users table in sys: <br />[\"@schema\":\"sys\"](http://apijson.cn:8080/get/{\"User\":{\"@schema\":\"sys\"}})<br /><br /> ⑦ Check Users table in PostgreSQL:<br />[\"@database\":\"POSTGRESQL\"](http://apijson.cn:8080/get/{\"User\":{\"@database\":\"POSTGRESQL\"}})<br /><br /> ⑧ Check the current user's activity:<br />[\"@role\":\"OWNER\"](http://apijson.cn:8080/get/{\"[]\":{\"Moment\":{\"@role\":\"OWNER\"}}})<br /><br /> ⑨ Turn on profiling: <br />[\"@explain\":true](http://apijson.cn:8080/get/{\"[]\":{\"Moment\":{\"@explain\":true}}})<br /><br /> ⑩ Get the No.0 picture from pictureList：<br />[\"@position\":0, //self-defined keyword<br />\"firstPicture()\":\"getFromArray(pictureList,@position)\"](http://apijson.cn:8080/get/{\"User\":{\"id\":38710,\"@position\":0,\"firstPicture()\":\"getFromArray(pictureList,@position)\"}})\n global keyword. | It is a keyword inside the outermost object {}. Among them, @database, @schema, @datasource, @role, and @explain are basically the same as object keywords, see the above description, the difference is that the global keywords will be automatically inserted in each table object as the default value. <br /><br />① \"tag\": String, the following tag is the identifier of the JSON structure matching the request in non-GET or HEAD requests, generally it is the name of the Table to be queried or the array Table[] or Table:[] corresponding to the name, determined by the backend specified in the Request table. <br /><br /> ② \"version\": Integer, the interface version. If the version is not passed, null or <=0, the highest version will be used. If other valid values are passed, the lowest version closest to it will be used, which is specified in the backend Request table.<br /><br /> ③ \"format\": Boolean, formatted to return the key of the Response JSON, generally converting TableName to tableName, TableName[] to tableNameList, Table:alias to alias, TableName-key[] to tableNameKeyList and other camelcase formats.  | ①  Check private information:：<br />[{\"tag\":\"Privacy\",\"Privacy\":{\"id\":82001}}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={%22tag%22:%22Privacy%22,%22Privacy%22:{%22id%22:82001}})<br /><br /> ② Use the version 1 interface to check private information:：<br />[{\"version\":1,\"tag\":\"Privacy\",\"Privacy\":{\"id\":82001}}](http://apijson.cn/api?url=http%3A%2F%2Fapijson.cn%3A8080%2Fgets&type=JSON&json={%22version%22:1,%22tag%22:%22Privacy%22,%22Privacy%22:{%22id%22:82001}}) <br /><br /> ③ Format Moments interface to return in JSON key：<br />[{<br > &nbsp;&nbsp; \"format\":true, <br > &nbsp;&nbsp; \"[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp; \"page\":0, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"count\":3, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"Moment\":{}, <br > &nbsp;&nbsp;&nbsp;&nbsp; \"User\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"id@\":\"/Moment/userId\"<br > &nbsp;&nbsp;&nbsp;&nbsp; },<br > &nbsp;&nbsp;&nbsp;&nbsp; \"Comment[]\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"count\":3,<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"Comment\":{<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \"momentId@\":\"[]/Moment/id\"<br > &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp;&nbsp;&nbsp; }<br > &nbsp;&nbsp; }<br >}](http://apijson.cn:8080/get/{\"format\":true,\"[]\":{\"page\":0,\"count\":3,\"Moment\":{},\"User\":{\"id@\":\"%252FMoment%252FuserId\"},\"Comment[]\":{\"count\":3,\"Comment\":{\"momentId@\":\"[]%252FMoment%252Fid\"}}}})\n\n<br />\n\n"
  },
  {
    "path": "LICENSE",
    "content": "Tencent is pleased to support the open source community by making APIJSON available.  \n\nCopyright (C) 2020 Tencent.  All rights reserved.\n\nAPIJSON is licensed under the Apache License Version 2.0.\nA copy of the Apache License Version 2.0 is included in this file.\n\nThe copyright notice pertaining to the Tencent code in this repo was previously in the name of “THL A29 Limited.”\nThat entity has now been de-registered.\nYou should treat all previously distributed copies of the code as if the copyright notice was in the name of “Tencent.”\n\nOther dependencies and licenses:\n\nOpen Source Software Licensed under the Apache License Version 2.0:\n--------------------------------------------------------------------\n\n\nTerms of  Apache License Version 2.0\n---------------------------------------------------\n\t\t\t\tApache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   \n"
  },
  {
    "path": "Navigation.md",
    "content": "# APIJSON 导航目录\n## [项目简介](/README.md#--apijson)\n\n### [特点功能](/README.md#%E7%89%B9%E7%82%B9%E5%8A%9F%E8%83%BD)\n#### [对于前端](/README.md#%E7%89%B9%E7%82%B9%E5%8A%9F%E8%83%BD)\n#### [对于后端](/README.md#%E5%AF%B9%E4%BA%8E%E5%90%8E%E7%AB%AF)\n<br />\n\n### [APIJSON 接口展示](/README.md#apijson-%E6%8E%A5%E5%8F%A3%E5%B1%95%E7%A4%BA)\n#### [Postman 展示 APIJSON](/README.md#postman-%E5%B1%95%E7%A4%BA-apijson)\n#### [APIAuto 展示 APIJSON](/README.md#apiauto-%E5%B1%95%E7%A4%BA-apijson)\n<br />\n\n### [APIJSON App 演示](/README.md#apijson-app-%E6%BC%94%E7%A4%BA)\n\n### [APIJSON 分享演讲](/README.md#apijson-%E5%88%86%E4%BA%AB%E6%BC%94%E8%AE%B2)\n\n### [为什么选择 APIJSON？](/README.md#%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9-apijson)\n<br />\n\n### [常见问题](/README.md#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)\n#### [1.如何定制业务逻辑？](/README.md#1%E5%A6%82%E4%BD%95%E5%AE%9A%E5%88%B6%E4%B8%9A%E5%8A%A1%E9%80%BB%E8%BE%91)\n#### [2.如何控制权限？](/README.md#2%E5%A6%82%E4%BD%95%E6%8E%A7%E5%88%B6%E6%9D%83%E9%99%90)\n#### [3.如何校验参数？](/README.md#3%E5%A6%82%E4%BD%95%E6%A0%A1%E9%AA%8C%E5%8F%82%E6%95%B0)\n<br />\n\n### [注意事项](/README.md#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)\n\n<br /><br />\n\n### [快速上手](/README.md#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B)\n#### [1.后端上手](/README.md#1%E5%90%8E%E7%AB%AF%E4%B8%8A%E6%89%8B)\n#### [2.前端上手](/README.md#2%E5%89%8D%E7%AB%AF%E4%B8%8A%E6%89%8B)\n<br />\n\n### [下载客户端 App](/README.md#%E4%B8%8B%E8%BD%BD%E5%AE%A2%E6%88%B7%E7%AB%AF-app)\n\n### [开源许可](/README.md#%E5%BC%80%E6%BA%90%E8%AE%B8%E5%8F%AF)\n\n### [使用登记](/README.md#%E4%BD%BF%E7%94%A8%E7%99%BB%E8%AE%B0)\n\n### [贡献者们](/README.md#%E8%B4%A1%E7%8C%AE%E8%80%85%E4%BB%AC)\n\n### [统计分析](/README.md#%E7%BB%9F%E8%AE%A1%E5%88%86%E6%9E%90)\n\n### [规划及路线图](/README.md#%E8%A7%84%E5%88%92%E5%8F%8A%E8%B7%AF%E7%BA%BF%E5%9B%BE)\n\n### [我要赞赏](/README.md#%E6%88%91%E8%A6%81%E8%B5%9E%E8%B5%8F)\n\n<br /><br />\n\n### [技术交流](/README.md#%E6%8A%80%E6%9C%AF%E4%BA%A4%E6%B5%81)\n\n### [相关推荐](/README.md#%E7%9B%B8%E5%85%B3%E6%8E%A8%E8%8D%90)\n\n### [生态项目](/README.md#%E7%94%9F%E6%80%81%E9%A1%B9%E7%9B%AED)\n\n### [持续更新](/README.md#%E6%8C%81%E7%BB%AD%E6%9B%B4%E6%96%B0)\n\n### [工蜂主页](/README.md#%E5%B7%A5%E8%9C%82%E4%B8%BB%E9%A1%B5)\n\n### [码云主页](/README.md#%E7%A0%81%E4%BA%91%E4%B8%BB%E9%A1%B5)\n"
  },
  {
    "path": "README-Chinese.md",
    "content": "Tencent is pleased to support the open source community by making APIJSON available.   <br/>\nCopyright (C) 2020 Tencent.  All rights reserved. <br/>\nThis source code is licensed under the Apache License Version 2.0 <br/>\n\n<h1 align=\"center\" style=\"text-align:center;\">\n  APIJSON\n</h1>\n\n<p align=\"center\"> 🏆 实时 零代码、全功能、强安全 ORM 库 🚀 <br />后端接口和文档零代码，前端(客户端) 定制返回 JSON 的数据和结构</p>\n\n<p align=\"center\" >\n  <a href=\"https://github.com/Tencent/APIJSON/blob/master/README.md\">English</a>\n  <a href=\"https://github.com/Tencent/APIJSON/blob/master/Document-Chinese.md\">通用文档</a>\n  <a href=\"https://search.bilibili.com/all?keyword=APIJSON\">视频教程</a>\n  <a href=\"http://apijson.cn/api\">测试用例</a>\n  <a href=\"https://deepwiki.com/Tencent/APIJSON\">AI 问答</a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/MySQL-5.7%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/PostgreSQL-9.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/SQLServer\"><img src=\"https://img.shields.io/badge/SQLServer-2012%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Oracle\"><img src=\"https://img.shields.io/badge/Oracle-12C%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/DB2\"><img src=\"https://img.shields.io/badge/DB2-7.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/MariaDB-10.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TiDB-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/CockroachDB-25.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/openGauss-5.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Dameng\"><img src=\"https://img.shields.io/badge/Dameng-7.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/Kingbase-8.6%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Milvus-2.2.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/DuckDB\"><img src=\"https://img.shields.io/badge/DuckDB-1.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONBoot-MultiDataSource\"><img src=\"https://img.shields.io/badge/SurrealDB-2.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/ClickHouse\"><img src=\"https://img.shields.io/badge/ClickHouse-21.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/PostGIS-3.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Elasticsearch\"><img src=\"https://img.shields.io/badge/Elasticsearch-7.17%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Manticore\"><img src=\"https://img.shields.io/badge/ManticoreSearch-7.4%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Presto\"><img src=\"https://img.shields.io/badge/Presto-0.277%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Trino\"><img src=\"https://img.shields.io/badge/Trino-400%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TDSQL-cloud-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TencentDB-cloud-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Redis\"><img src=\"https://img.shields.io/badge/Redis-5.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Kafka\"><img src=\"https://img.shields.io/badge/Kafka-3.2%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Snowflake-7.0%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Databricks-13.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/chenyanlann/APIJSONBoot_Hive\"><img src=\"https://img.shields.io/badge/Hive-3.1%2B-brightgreen.svg?style=flat\"></a>  \n  <a href=\"https://github.com/chenyanlann/APIJSONBoot_Hive\"><img src=\"https://img.shields.io/badge/Hadoop-3.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/MongoDB-Altlas%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Cassandra-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/InfluxDB-2.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/TDengine\"><img src=\"https://img.shields.io/badge/TDengine-2.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/TimescaleDB-17.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/IoTDB-1.3%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONBoot-MultiDataSource\" ><img src=\"https://img.shields.io/badge/DataBend-1.2%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server\"><img src=\"https://img.shields.io/badge/Java-1.8%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/glennliao/apijson-go\"><img src=\"https://img.shields.io/badge/Go-1.18%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/j2go/apijson-go\"><img src=\"https://img.shields.io/badge/Go-1.16%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/liaozb/APIJSON.NET\"><img src=\"https://img.shields.io/badge/CSharp-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/kvnZero/hyperf-APIJSON\"><img src=\"https://img.shields.io/badge/PHP-8.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/kevinaskin/apijson-node\"><img src=\"https://img.shields.io/badge/Node.js-ES6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/zhangchunlin/uliweb-apijson\"><img src=\"https://img.shields.io/badge/Python-3%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://gitee.com/APIJSON/panda-base\"><img src=\"https://img.shields.io/badge/Rust-1.90%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-Script\"><img src=\"https://img.shields.io/badge/Lua-5.2%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONDemo/pom.xml#L48-L52\"><img src=\"https://img.shields.io/badge/Spring-4.3.2%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONDemo/pom.xml#L48-L52\"><img src=\"https://img.shields.io/badge/SpringBoot-1.4.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONFinal/pom.xml#L59-L68\"><img src=\"https://img.shields.io/badge/JFinal-3.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/vincent109/apijson-nutz\"><img src=\"https://img.shields.io/badge/Nutz-2.4.2%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-ShardingSphere\"><img src=\"https://img.shields.io/badge/ShardingSphere-5.4.1%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Android\"><img src=\"https://img.shields.io/badge/Android-4.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-iOS\"><img src=\"https://img.shields.io/badge/iOS-7%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-JavaScript\"><img src=\"https://img.shields.io/badge/JavaScript-ES6%2B-brightgreen.svg?style=flat\"></a>\n</p>\n\n<p align=\"center\" >\n  <img src=\"https://oscimg.oschina.net/oscnet/up-3299d6e53eb0534703a20e96807727fac63.png\" />\n</p>\n\n---\n\n<b >导航目录：</b> 项目简介 [上手使用](#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) [社区生态](#%E6%8A%80%E6%9C%AF%E4%BA%A4%E6%B5%81)  &nbsp;&nbsp;&nbsp;&nbsp;  完整详细的导航目录 [点这里查看](/Navigation.md) <br />\n\n\nAPIJSON 是一种专为 API 而生的 JSON 网络传输协议 以及 基于这套协议实现的 ORM 库。<br />\n为各种增删改查提供了完全自动化的万能通用接口，零代码实时满足千变万化的各种新增和变更需求。<br />\n能大幅降低开发和沟通成本，简化开发流程，缩短开发周期。<br />\n适合中小型前后端分离的项目，尤其是 创业项目、内部项目、低代码/零代码、小程序、BaaS、Serverless 等。<br />\n\n通过万能通用接口，前端可以定制任何数据、任何结构。<br />\n大部分 HTTP 请求后端再也不用写接口了，更不用写文档了。<br />\n前端再也不用和后端沟通接口或文档问题了。再也不会被文档各种错误坑了。<br />\n后端再也不用为了兼容旧接口写新版接口和文档了。再也不会被前端随时随地没完没了地烦了。\n\n### 特点功能\n\n#### 对于后端\n* 提供万能通用接口，大部分 HTTP API 不用再写\n* 零代码增删改查、各种跨库连表、JOIN 嵌套子查询等\n* 自动生成文档，不用再编写和维护，且自动静态检查\n* 自动校验权限、自动管理版本、自动防 SQL 注入\n* 开放 HTTP API 无需划分版本，始终保持兼容\n\n#### 对于前端\n* 不用再向后端催接口、求文档\n* 数据和结构完全定制，要啥有啥\n* 看请求知结果，所求即所得\n* 可一次获取任何数据、任何结构\n* 能去除多余数据，节省流量提高速度\n\n<br />\n\n### APIJSON 接口展示\n#### Postman 展示 APIJSON\n![](https://static.oschina.net/uploads/img/201711/12230359_f7fQ.jpg)\n<br/>\n\n#### APIAuto 展示 APIJSON\n**使用 APIAuto-机器学习接口工具 来管理和测试 HTTP API 可大幅 减少传参错误、提升联调效率** <br/>\n(注意网页工具界面是 APIAuto，里面的 URL+JSON 才是 APIJSON 的 HTTP API)： <br/>\n<br />\n<p align=\"center\" >\n  <a >APIJSON 多表关联查询、结构自由组合，APIAuto 多个测试账号、一键共享测试用例</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-bbbec4fc5edc472be127c02a4f3cd8f4ec2.JPEG) \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_associate.gif) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto 自动生成前端(客户端)请求代码 和 Python 测试用例代码，一键下载</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-637193bbd89b41c3264827786319e842aee.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto 自动保存请求记录、自动生成接口文档，可添加常用请求、快捷查看一键恢复</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-7dcb4ae71bd3892a909e4ffa37ba7c1d92a.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto 一键自动接口回归测试，不需要写任何代码(注解、注释等全都不要)</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-c1ba774f8e7fcc5adcdb05cad5bd414d766.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >一图胜千言 - APIJSON 部分基础功能概览</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-e21240ef3770326ee6015e052226d0da184.JPEG) \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_summary.gif) \n\n\n<br /><br />\n\n### APIJSON App 演示\n使用 APIJSON + ZBLibrary 开发的 Android 客户端 Demo (以下 Gif 图看起来比较卡，实际上运行很流畅)：\n<br />\n![](https://oscimg.oschina.net/oscnet/up-a3f167e593080e8a3fc09c3d5fc09330c98.gif) \n![](https://oscimg.oschina.net/oscnet/up-141abcb5dabc01c890d70c461bd1fdc751f.gif) \n![](https://oscimg.oschina.net/oscnet/up-58aecc2701c2c4ea33e53f246e427773b09.gif)\n\n<br />\n\n### APIJSON 分享演讲\n#### APIJSON-零代码接口与文档 ORM 库（国际开源谷 Gitee Meetup）\n\nhttps://www.bilibili.com/video/BV1Tv411t74v\n\n![image](http://apijson.cn/images/comparison/APIJSON_vs_PreviousWays.jpg)\n\n\n#### APIJSON 和 APIAuto-零代码开发和测试（QECon 全球软件质量&效能大会）\n\nhttps://www.bilibili.com/video/BV1yv411p7Y4\n\n<img width=\"1360\" alt=\"wecom-temp-377bbd0daf5aed716baf7ebcb003d94c\" src=\"https://user-images.githubusercontent.com/5738175/121370207-1b35de00-c96f-11eb-840e-cc2ff2995888.png\">\n\n\n<br />\n\n### 为什么选择 APIJSON？\n前后端 关于接口的 开发、文档、联调 等 10 大痛点解析 <br />\nhttps://github.com/Tencent/APIJSON/wiki\n\n* **解决十大痛点** (可帮前后端开发大幅提振开发效率、强力杜绝联调扯皮、巧妙规避文档缺陷、非常节省流量带宽)\n* **开发提速很大** (CRUD 零代码热更新全自动，APIJSONBoot 对比 SSM、SSH 等保守估计可提速 20 倍以上)\n* **腾讯官方开源** (使用 GitHub、Gitee、工蜂 等平台的官方账号开源，微信公众号、腾讯云+社区 等官方公告)\n* **社区影响力大** (GitHub 18K+ Star 在 400W Java 项目排名前 100，远超 FLAG, BAT 等国内外绝大部分开源项目)\n* **各项荣誉成就** (腾讯内外 5 个奖项、腾讯开源前五、腾讯后端 Star 第一、Trending 日周月榜大满贯 等)\n* **多样用户案例** (腾讯内有互娱、音乐、微信、云与智慧，外部有华为、华能、百度、快手、中兴、圆通、传音等)\n* **适用场景广泛** (社交聊天、阅读资讯、影音娱乐、办公学习 等各种 App、网站、小程序、公众号 等非金融类项目)\n* **周边生态丰富** (Android, iOS, Web 等各种 Demo、继承 JSON 的海量生态、零代码 接口测试 和 单元测试 工具等)\n* **文档视频齐全** (项目介绍、快速上手、安装部署 等后端、前端、客户端的 图文解说、视频教程、代码注释 等)\n* **功能丰富强大** (增删改查、分页排序、分组聚合、各种条件、各种 JOIN、各种子查询、跨库连表 等零代码实现)\n* **使用安全简单** (自动增删改查、自动生成文档、自动管理版本、自动控制权限、自动校验参数、自动防 SQL 注入)\n* **灵活定制业务** (在后端编写 远程函数，可以拿到 session、version、当前 JSON 对象 等，然后自定义处理)\n* **高质可靠代码** (代码严谨规范，蚂蚁集团源伞 Pinpoint 代码扫描分析报告平均每行代码 Bug 率低至 0.15%)\n* **兼容各种项目** (协议不限 HTTP，与其它库无冲突，对各类 Web 框架集成友好且提供 SpringBoot, JFinal 的示例)\n* **工程轻量小巧** (无第三方依赖，Jar 仅 263KB，Java 文件仅 68 个共 14864 行代码，例如 APIJSONORM 8.1.0)\n* **多年持续迭代** (自 2016 年起已连续维护 8 年多，70+ 贡献者、100+ 发版、3000+ 提交，不断更新迭代中...)\n\n**按照一般互联网中小型项目情况可得出以下对比表格：**\n\n表数量 T | 平均每表字段数 C |         SSMH 按快估计     | APIJSONBoot 按慢估计 | APIJSONBoot 提速倍数\n--------  |   ---------  |  ---------------------  |  ------------------ |  -----------\n1         |    3         |  179 min(约一上午)        | 11 min(约十分钟)     |  15.27\n5         |    4         |  1935 min(约朝九晚六一周)  | 70 min(约一小时)     |  26.64\n10        |    10        |  8550 min(大小周超半个月)  | 320 min(约一下午)    |  25.72\n20        |    15        |  31900 min(约 996 两个月) | 940 min(约上班两天)  |  32.94\n50        |    20        |  176750 min(11117 超半年) | 3100 min(约上班一周) |  56.02             \n \n### 用户反馈\n**腾讯 IEG 数据产品开发组负责人 xinlin：**\n“腾讯的 APIJSON 开源方案，它可以做到零代码生成接口和文档，并且整个生成过程是自动化。当企业有元数据的时候，马上就可以获得接口”\n\n**腾讯科技 后台开发高级工程师 雷大锤：**\n“可以抽出时间来看apijson了，这个可以为T10做准备，也是业界很火的东西，可以提升个人影响力！”\n\n**腾讯 bodian520：**\n“在调试GET、POST、PUT接口时遇到了一些问题，把个人的摸索经验分享一下，希望作者能梳理下文档，方便我们更好的接入”\n\n**华为 minshiwu：**\n“demo工程，默认使用apijson-framework，可以做到无任何配置即可体验apijson的各种能力。”\n\n**字节跳动 qiujunlin：**\n“初次见到这个项目，觉得太惊艳了，眼前一亮。给我的感受是，项目大大简化了开发流程，开发效率提升了很多倍。”\n\n**百度智慧城市研发 lpeng：**\n“很兴奋的发现APIJSON很适合我们的一个开发场景，作为我们协议定义的一部分” \n\n**中兴 duyijiang：**\n“感谢腾讯大大提供的框架，很好用”\n\nhttps://github.com/Tencent/APIJSON/issues/132#issuecomment-1106669540\n\n<br />\n\n### 常见问题\n#### 1.如何定制业务逻辑？\n在后端编写 远程函数，可以拿到 session、version、当前 JSON 对象、参数名称 等，然后对查到的数据自定义处理 <br />\nhttps://github.com/Tencent/APIJSON/issues/101\n\n#### 2.如何控制权限？\n在 Access 表配置校验规则，默认不允许访问，需要对 每张表、每种角色、每种操作 做相应的配置，粒度细分到行级 <br />\nhttps://github.com/Tencent/APIJSON/issues/12\n\n#### 3.如何校验参数？\n在 Request 表配置校验规则 structure，提供 MUST、TYPE、VERIFY 等通用方法，可通过 远程函数 来完全自定义 <br />\nhttps://github.com/Tencent/APIJSON/wiki#%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86\n\n更多常见问题及提问前必看 <br />\nhttps://github.com/Tencent/APIJSON/issues/36\n<br />\n\n### 注意事项\n**请求参数 JSON 中表名、字段名、关键词及对应的值都是大小写敏感、逗号敏感、分号敏感、空格敏感、换行敏感， <br />\n大部分情况都不允许空格和换行，表名以大写字母开头，不要想当然，请严格按照 [设计规范](https://github.com/Tencent/APIJSON/blob/master/Document.md#3) 来调用 API ！**\n[#181](https://github.com/Tencent/APIJSON/issues/181)\n<br />\n<br />\n<br />\n<br />\n\n<b >导航目录：</b> [项目简介](#--apijson) 上手使用 [社区生态](#%E6%8A%80%E6%9C%AF%E4%BA%A4%E6%B5%81)  &nbsp;&nbsp;&nbsp;&nbsp;  完整详细的导航目录 [点这里查看](/Navigation.md) <br />\n\n### 快速上手\n\n#### 1.后端上手\n可以跳过这个步骤，直接用APIJSON服务器IP地址 apijson.cn:8080 来测试接口。<br />\n见&nbsp; [APIJSON后端上手 - Java](https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server)<br />\n\n#### 2.前端上手\n可以跳过这个步骤，直接使用 [APIAuto-机器学习HTTP接口工具](https://github.com/TommyLemon/APIAuto) 或 下载客户端App。<br />\n见&nbsp; [Android](https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Android) &nbsp;或&nbsp; [iOS](https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-iOS) &nbsp;或&nbsp; [JavaScript](https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-JavaScript)<br />\n\n\n### 下载客户端 App\n\n仿微信朋友圈动态实战项目<br />\n[APIJSONApp.apk](http://files.cnblogs.com/files/tommylemon/APIJSONApp.apk)\n\n测试及自动生成代码工具<br />\n[APIJSONTest.apk](http://files.cnblogs.com/files/tommylemon/APIJSONTest.apk)\n\n### 开源许可\n使用 [Apache License 2.0](/LICENSE)，对 公司、团队、个人 等 商用、非商用 都自由免费且非常友好，请放心使用和登记\n\n### 使用登记\n如果您在使用 APIJSON，请让我们知道，您的使用对我们非常重要(新的按登记顺序排列、专群优先答疑解惑)：<br />\nhttps://github.com/Tencent/APIJSON/issues/187 \n<div style=\"float:left\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/126525534-461c3e33-57b1-4630-af7f-f1238ca4ab98.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/126525251-c05e64c6-6b60-4457-a46e-dea7dcfb80cd.png\" height=\"75\">\n  <br />\n  <img src=\"https://user-images.githubusercontent.com/5738175/195065513-c581e958-2386-4a34-8b78-f48e87d1e1f2.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195063764-dcc272a0-3c2c-4073-8f22-c501c22a0844.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195063874-9d37425d-f220-445f-8554-655d5c02931b.png\" height=\"75\">\n  <br />\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.transsion.com.jpeg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/shebaochina.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.xmfish.com.gif\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.xxwolo.com.jpeg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/t-think.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/xm.juhu.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.aipaipai-inc.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.8sso.com.jpeg\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758356-fbc89569-8b34-49d4-9f8e-272a8406440d.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.shulian8.com.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758846-1c055ae1-c235-498b-a64c-902a6068af76.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.hngtrust.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.hec-bang.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.toutou.com.cn.jpg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.yto.net.cn.jpg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.lepinyongche.com.jpg\"  height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.aupup.com.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758697-3267f031-a7bc-44f2-84bb-06a4a7e30a75.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758188-40294d75-ef7d-4ddc-9af8-5b8c195839cf.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758198-8ec01213-18f7-43d5-9942-7c49a898ccef.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/95326431/194802562-e7f92b39-edbb-401f-806a-1a22513e785e.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758742-28d79efd-6645-44ee-bb50-844aa39b25fe.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758753-0a3bb998-a533-4388-8224-4f9d743ff576.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758795-e49e3eae-12ba-4399-a8e1-75db94cb0a99.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758984-0fe2fcd9-5119-46d3-9e22-4632556c0b9e.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758995-db762406-627b-4ea5-8397-b99bb5711cce.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759031-bdcf4146-34cb-470c-a576-37d4e8fdca24.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759040-c7db99ff-3404-411d-b9ba-23547aaf1509.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759093-927fd5c3-9e1e-4648-8a35-c9d97630d086.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/10636d69-1d54-4666-aa8a-472c4ecb9413\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759079-ffc4483e-46a6-4e28-a0e0-25186ea008ab.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759186-a90a04db-0bd4-47bc-bab0-c160dcf48e53.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759204-7bdb09f5-2194-41c1-8e59-1461bd5ff4c1.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759227-2e5d42ae-b42d-4702-801d-566e70809e79.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759318-b0edad0d-9f6c-44b9-97a4-6c566880bc4b.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759239-1cb44526-abfa-4800-8d65-233d04b7c0d3.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759268-b4ad2945-704e-495c-b2b0-d0166dc5e33a.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759292-baa3924c-cf56-49cf-820c-d1e0a88cac3b.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652404-927a945e-22f5-42f8-99da-3a0863a5a3b5.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226653817-fcc57051-53e2-4c8d-bda6-3effba4032ee.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/7c71b8f9-f1cc-4305-8e97-c212f476e377\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652405-561963cb-73e4-4d65-986c-ebfafcfe7b73.jpeg\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652403-92546c06-6dc4-4f46-b697-02a4073833f8.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226657098-d63c0dd1-24d0-4819-9045-b8213ab2e31f.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226657183-b6bbf284-3eb4-431e-8549-6356b7929e45.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/9de7c199-4f29-44ed-9cb6-ad5e4fa44dfa\" height=\"90\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/d7155a65-22f7-49c6-8354-c309f36e4065\" height=\"60\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/f5a6ec8d-d9a8-49d0-a284-c50f1376647e\" height=\"75\">\n  <img src=\"https://gitee.com/linksame-ivan/APIJSON/blob/master/assets/logo-name.9f99700f.png\" height=\"75\">\n<br />\n  \n * [腾讯科技有限公司](https://www.tencent.com)\n * [腾讯音乐娱乐集团](https://www.tencentmusic.com)\n * [深圳市传音通讯有限公司](https://www.transsion.com)\n * [社宝信息科技（上海）有限公司](https://shebaochina.com)\n * [华能贵诚信托有限公司](https://www.hngtrust.com)\n * [投投科技](https://www.toutou.com.cn)\n * [圆通速递](https://www.yto.net.cn)\n * [乐拼科技](https://www.lepinyongche.com)\n * [珠海采筑电子商务有限公司](https://www.aupup.com)\n * [爱投斯智能技术（深圳）有限公司](http://www.aiotos.net)\n * [邻盛科技（武汉）有限公司](http://www.linksame.com)\n * [上海麦市信息科技有限公司](https://www.masscms.com)\n * [上海翊丞互联网科技有限公司](http://www.renrencjl.com/home)\n * [上海直真君智科技有限公司](http://www.zzjunzhi.com)\n * [北明软件有限公司](https://www.bmsoft.com.cn/)\n * [上海钰亿环保科技有限公司](#)\n\n### 贡献者们\n主项目 APIJSON 的贡献者们(6 个腾讯工程师、1 个微软工程师、1 个阿里云工程师、1 个字节跳动工程师、1 个网易工程师、1 个 Zoom 工程师、1 个圆通工程师、1 个知乎基础研发架构师、1 个智联招聘工程师、gorm-plus 作者、1 个美国加州大学学生、3 个 SUSTech 学生等)：<br />\nhttps://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md <br />\n<div style=\"float:left\">\n  <a href=\"https://github.com/TommyLemon\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\" \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/41\"><img src=\"https://avatars0.githubusercontent.com/u/39320217?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/119\"><img src=\"https://avatars1.githubusercontent.com/u/25604004?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/223\"><img src=\"https://avatars.githubusercontent.com/u/49295281?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/43\"><img src=\"https://avatars0.githubusercontent.com/u/23173448?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/47\"><img src=\"https://avatars2.githubusercontent.com/u/31512287?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/212\"><img src=\"https://avatars.githubusercontent.com/u/8936328?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/70\"><img src=\"https://avatars1.githubusercontent.com/u/22228201?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/74\"><img src=\"https://avatars0.githubusercontent.com/u/1274536?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/92\"><img src=\"https://avatars3.githubusercontent.com/u/6327228?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/103\"><img src=\"https://avatars0.githubusercontent.com/u/25990237?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/194\"><img src=\"https://avatars0.githubusercontent.com/u/3982329?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/219\"><img src=\"https://avatars.githubusercontent.com/u/7135770?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/222\"><img src=\"https://avatars.githubusercontent.com/u/49233056?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/221\"><img src=\"https://avatars.githubusercontent.com/u/17545585?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/217\"><img src=\"https://avatars.githubusercontent.com/u/30771966?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/190\"><img src=\"https://avatars3.githubusercontent.com/u/25056767?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/69\"><img src=\"https://avatars0.githubusercontent.com/u/13880474?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/72\"><img src=\"https://avatars1.githubusercontent.com/u/10663804?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/33\"><img src=\"https://avatars1.githubusercontent.com/u/5328313?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/235\"><img src=\"https://avatars.githubusercontent.com/u/17243165?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/233\"><img src=\"https://avatars.githubusercontent.com/u/1252459?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/250\"><img src=\"https://avatars.githubusercontent.com/u/44310040?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/253\"><img src=\"https://avatars.githubusercontent.com/u/19265050?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/278\"><img src=\"https://avatars.githubusercontent.com/u/4099373?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/279\"><img src=\"https://avatars.githubusercontent.com/u/28685375?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/280\"><img src=\"https://avatars.githubusercontent.com/u/60541766?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/283\"><img src=\"https://avatars.githubusercontent.com/u/50007106?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/284\"><img src=\"https://avatars.githubusercontent.com/u/45117061?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/285\"><img src=\"https://avatars.githubusercontent.com/u/32100214?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/287\"><img src=\"https://avatars.githubusercontent.com/u/62465397?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/288\"><img src=\"https://avatars.githubusercontent.com/u/55579125?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/291\"><img src=\"https://avatars.githubusercontent.com/u/17522475?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/293\"><img src=\"https://avatars.githubusercontent.com/u/53826144?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/295\"><img src=\"https://avatars.githubusercontent.com/u/11210385?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/311\"><img src=\"https://avatars.githubusercontent.com/u/22066942?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/325\"><img src=\"https://avatars.githubusercontent.com/u/33931153?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/443\"><img src=\"https://avatars.githubusercontent.com/u/95326431?s=40&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/447\"><img src=\"https://avatars.githubusercontent.com/u/46688?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/452\"><img src=\"https://avatars.githubusercontent.com/u/45644893?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/456\"><img src=\"https://avatars.githubusercontent.com/u/14543369?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/472\"><img src=\"https://avatars.githubusercontent.com/u/22633385?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/480\"><img src=\"https://avatars.githubusercontent.com/u/35208417?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/481\"><img src=\"https://avatars.githubusercontent.com/u/12228225?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/483\"><img src=\"https://avatars.githubusercontent.com/u/46614808?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/489\"><img src=\"https://avatars.githubusercontent.com/u/3324416?s=60&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/498\"><img src=\"https://avatars.githubusercontent.com/u/41458196?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/505\"><img src=\"https://avatars.githubusercontent.com/u/30490359?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/535\"><img src=\"https://avatars.githubusercontent.com/u/68573559?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/543\"><img src=\"https://avatars.githubusercontent.com/u/32206192?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/545\"><img src=\"https://avatars.githubusercontent.com/u/28621460?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/610\"><img src=\"https://avatars.githubusercontent.com/u/33263203?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/616\"><img src=\"https://avatars.githubusercontent.com/u/42954433?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/668\"><img src=\"https://avatars.githubusercontent.com/u/102006886?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/684\"><img src=\"https://avatars.githubusercontent.com/u/159116925?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/685\"><img src=\"https://avatars.githubusercontent.com/u/44639807?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/691\"><img src=\"https://avatars.githubusercontent.com/u/19484446?s=96&v=4\"  height=\"54\" width=\"54\" ></a>\n</div>\n<br />\n\n生态周边项目的作者们(2 个腾讯工程师、1 个 BAT 技术专家、1 个微软工程师、2 个字节跳动工程师、1 个神州数码工程师&Apache dubbo2js 作者 等)：<br />\nhttps://github.com/search?o=desc&q=apijson&s=stars&type=Repositories <br />\nhttps://search.gitee.com/?skin=rec&type=repository&q=apijson&sort=stars_count <br />\n<div style=\"float:left\">\n  <a href=\"https://github.com/APIJSON/apijson-orm\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/liaozb/APIJSON.NET\"><img src=\"https://avatars3.githubusercontent.com/u/12622501?s=400&v=4\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/tiangao/apijson-go\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/43/130007_tiangao_1578918889.png!avatar200\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/qq547057827/apijson-php\"><img src=\"https://avatars3.githubusercontent.com/u/1657532?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/xianglong111/APIJSON-php\"><img src=\"https://avatars.githubusercontent.com/u/9738743?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/kevinaskin/apijson-node\"><img src=\"https://avatars3.githubusercontent.com/u/20034891?s=400&v=4\"\n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TEsTsLA/apijson\"><img src=\"https://avatars2.githubusercontent.com/u/17310639?s=400&v=4\"\n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/zhangchunlin/uliweb-apijson\"><img src=\"https://avatars0.githubusercontent.com/u/359281?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/crazytaxi824/APIJSON\"><img src=\"https://avatars3.githubusercontent.com/u/16500384?s=400&v=4\" \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/luckyxiaomo/APIJSONKOTLIN\"><img src=\"https://avatars2.githubusercontent.com/u/42728605?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Zerounary/APIJSONParser\"><img src=\"https://avatars2.githubusercontent.com/u/31512287?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson-framework\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson-column\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/jerrylususu/apijson_todo_demo\"><img src=\"https://avatars.githubusercontent.com/u/17522475?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/vcoolwind/apijson-practice\"><img src=\"https://avatars.githubusercontent.com/u/22070287?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/rainboy-learn/apijson-learn\"><img src=\"https://avatars.githubusercontent.com/u/43025876?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/greyzeng/apijson-sample\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/367/1102309_greyzeng_1578940307.png!avatar200\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/zhiyuexin/ApiJsonByJFinal\"><img src=\"https://avatar.gitee.com/uploads/90/490_zhiyuexin.jpg!avatar100?1368664499\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Airforce-1/SpringServer1.2-APIJSON\"><img src=\"https://avatars3.githubusercontent.com/u/6212428?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/JinShuProject/JinShuApiJson\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/232/698672_maxiaoji_1578931055.jpg!avatar200\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/qiujunlin/APIJSONDemo\"><img src=\"https://avatars.githubusercontent.com/u/50007106?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/chenyanlann/APIJSONDemo_ClickHouse\"><img src=\"https://avatars.githubusercontent.com/u/62465397?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/andream7/apijson-db2\"><img src=\"https://avatars.githubusercontent.com/u/60541766?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/APIJSON-Android-RxJava\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/APIAuto\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/UnitAuto\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/vincentCheng/apijson-doc\"><img src=\"https://avatars3.githubusercontent.com/u/6327228?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/ruoranw/APIJSONdocs\"><img src=\"https://avatars.githubusercontent.com/u/25990237?s=460&u=2143b95e5ed39185f2a03d66fbb5638795e16d5a&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/HANXU2018/APIJSON-DOC\"><img src=\"https://avatars.githubusercontent.com/u/45117061?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/kenlig/apijsondocs\"><img src=\"https://avatars.githubusercontent.com/u/28685375?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson.org\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/pengxianggui/apijson-builder\"><img src=\"https://avatars2.githubusercontent.com/u/16299169?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/creasy2010/apijson-docker\"><img src=\"https://avatars.githubusercontent.com/u/1592277?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/AbsGrade\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/Android-ZBLibrary\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n</div>\n<br />\n\n还有为 APIJSON 扫描代码贡献 Issue 的 [蚂蚁集团源伞](https://www.sourcebrella.com) 和 [奇安信代码卫士](https://github.com/QiAnXinCodeSafe)\n<div style=\"float:left\">\n  <a href=\"https://www.sourcebrella.com\" style=\"padding: 5px\"><img src=\"https://github.com/user-attachments/assets/2d580664-9df9-4275-8346-5ba2d04936b9\" height=\"90\" ></a>\n  <a href=\"https://github.com/QiAnXinCodeSafe\" style=\"margin-right: 20px\"><img src=\"https://avatars.githubusercontent.com/u/39950310?s=460&u=8c4ad8c2174ba78c0604614a6e2feced07521ce6&v=4\"  height=\"90\" ></a>\n</div>\n<br />\n\n感谢大家的贡献。\n\n### 统计分析 \n腾讯、华为、阿里巴巴、美团、字节跳动、百度、京东、网易、快手等和 Google, Apple, Microsoft, Amazon, Paypal, IBM, Shopee 等 <br >\n数百名知名大厂员工点了 Star，也有腾讯、华为、字节跳动、Microsoft、Zoom 等不少知名大厂员工提了 PR/Issue，感谢大家的支持~ <br >\n[![Stargazers over time](https://starchart.cc/Tencent/APIJSON.svg)](https://starchart.cc/Tencent/APIJSON)\n<img width=\"948\" alt=\"image\" src=\"https://github.com/Tencent/APIJSON/assets/5738175/2784e399-11c8-4eeb-8257-44533df61827\">\n<img width=\"948\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/195752839-554d0204-aa5d-48d8-b838-d1a0cb0e8690.png\">\n<img width=\"948\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/195752907-a09d9505-beb3-47a6-b7b9-079b58964b4d.png\">\n\n根据开源指南针报告，APIJSON Java 版已经是国内顶级、国际一流的 Java 开源项目了 [#518](https://github.com/Tencent/APIJSON/issues/518) <br >\n<img width=\"1495\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/227158515-4e8bab65-a4b9-402b-b60b-29c08b4d930d.png\">\n\n### 规划及路线图\n新增功能、强化安全、提高性能、增强稳定、完善文档、丰富周边、推广使用 <br />\nhttps://github.com/Tencent/APIJSON/blob/master/Roadmap.md\n\n理论上所有支持 SQL 与 JDBC/ODBC 的软件，都可以用本项目对接 CRUD，待测试: <br />\n[OceanBase](https://www.oceanbase.com/docs/oceanbase/V2.2.50/ss-sr-select_daur3l), [Spark](https://spark.apache.org/docs/3.3.0/sql-ref-syntax-qry-select.html)(可用 Hive 对接), [Phoenix](http://phoenix.apache.org/language/index.html#select)(延伸支持 HBase)\n\n### 我要赞赏\n创作不易，坚持更难，右上角点 ⭐Star 来支持/收藏下吧，谢谢 ^_^ <br />\nhttps://github.com/Tencent/APIJSON\n  \n<br />\n<br />\n<br />\n\n<b >导航目录：</b> [项目简介](#--apijson) [上手使用](#%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B) 社区生态  &nbsp;&nbsp;&nbsp;&nbsp;  完整详细的导航目录 [点这里查看](/Navigation.md)<br />\n\n### 技术交流\n如果有什么问题或建议可以 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/Tencent/APIJSON/issues/36)，交流技术，分享经验。 <br >\n如果你解决了某些 bug，或者新增了一些功能，欢迎 [贡献代码](https://github.com/Tencent/APIJSON/pulls)，感激不尽~ <br >\nhttps://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md\n  \n**开发者也是人，也需要工作、休息、恋爱、陪伴家人、走亲会友等，也有心情不好和身体病痛，** <br > \n**往往没有额外的时间精力顾及一些小问题，请理解和支持，开源要大家参与贡献才会更美好~** <br >\n**少数个人的热情终有被耗尽的一天，只有大家共同建设和繁荣社区，才能让开源可持续发展！** <br >  \n\n**开发者时间精力有限，原则上优先解决 登记用户 和 贡献者 的问题，** <br >\n**不解决 文档/视频/常见问题 已明确说明、描述简陋 或 态度无礼 的问题！** <br >\n**如果你已经多次得到帮助，却仍然只索取不贡献，那就别指望大家再帮你！** <br >\n**私聊作者请教技术问题 或者 频繁在互助群 @ 作者 可能会被拉黑/禁言/踢群，请尊重和理解，谢谢！** <br >\n\n如果你 [提 PR 登记了自己使用 APIJSON 的公司](https://github.com/Tencent/APIJSON/issues/187)，可以加 **企业用户支持专群**，作者亲自且优先答疑，<br >\n作者只有解答完了这个专群里的全部问题，才看情况解答 Issue/问卷 里的问题(对 Issue/问卷 不保证解答、更不保证及时)；<br >\n之前的几个互助群，由于大多数问题 在文档/Issue 已有答案却反复提 或者 缺少必要信息要来来回回沟通问清细节 已浪费太多时间，<br >\n甚至有白嫖还把自己当大爷的自私自利伸手党输出情绪，我们不再支持，建议未登记企业的用户 [填问卷](https://wj.qq.com/s2/10971431/2a09) 或 [提 Issue](https://github.com/Tencent/APIJSON/issues/36)。<br >\n\n如果你为 APIJSON 做出了以下任何一个贡献，我们将优先为你答疑解惑： <br >\n[提交了 PR 且被合并](https://github.com/Tencent/APIJSON/pull/92)、[提交了优质 Issue](https://github.com/Tencent/APIJSON/issues/189)、[发表了优质文章](https://blog.csdn.net/qq_41829492/article/details/88670940)、[开发了可用的生态项目](https://github.com/zhangchunlin/uliweb-apijson)， <br >\nIssue/问卷 一般解答顺序：贡献者 > 帮助他人的用户 > 提供任职企业的用户 > 其他用户。\n  \n\n### 相关推荐\n[APIJSON, 接口和文档的终结者！](https://my.oschina.net/tommylemon/blog/805459)\n\n[腾讯业务百万数据 6s 响应，APIJSON 性能优化背后的故事](https://my.oschina.net/tommylemon/blog/5375645)  \n\n[仿QQ空间和微信朋友圈，高解耦高复用高灵活](https://my.oschina.net/tommylemon/blog/885787)\n\n[后端开挂:3行代码写出8个接口！](https://my.oschina.net/tommylemon/blog/1574430)\n\n[后端自动化版本管理，再也不用改URL了！](https://my.oschina.net/tommylemon/blog/1576587)\n\n[3步创建APIJSON后端新表及配置](https://my.oschina.net/tommylemon/blog/889074)\n\n[APIJSON对接分布式HTAP数据库TiDB](https://my.oschina.net/tommylemon/blog/3081913)\n\n[APIJSON教程（一）：上手apijson项目，学习apijson语法，并实现持久层配置](https://zhuanlan.zhihu.com/p/375681893)\n\n[apijson简单demo](https://blog.csdn.net/dmw412724/article/details/113558115)\n\n[apijson简单使用](https://www.cnblogs.com/greyzeng/p/14311995.html)\n\n[APIJSON简单部署和使用](https://blog.csdn.net/m450744192/article/details/108462611)\n\n[学习自动化接口APIJSON](https://www.jianshu.com/p/981a2a630c7b)\n\n[APIJSON 接口调试实践](https://github.com/Tencent/APIJSON/issues/189)\n    \n[APIJSON-零代码接口和文档 JSON 协议 与 ORM 库](https://cloud.tencent.com/developer/article/2077042)\n\n[APIJSON使用例子总结](https://blog.csdn.net/weixin_41077841/article/details/110518007)\n\n[APIJSON 自动化接口和文档的快速开发神器 （一）](https://blog.csdn.net/qq_41829492/article/details/88670940)\n\n[APIJSON在mac电脑环境下配置去连接SQL Server](https://juejin.im/post/5e16d21ef265da3e2e4f4956)\n\n[APIJSON复杂业务深入实践（类似12306订票系统）](https://blog.csdn.net/aa330233789/article/details/105309571)\n\n[新手搭建 APIJSON 项目指北](https://github.com/jerrylususu/apijson_todo_demo/blob/master/FULLTEXT.md)  \n\n[使用APIJSON写低代码Crud接口](https://blog.csdn.net/weixin_42375862/article/details/121654264)\n  \n[apijson在同一个接口调用中 使用远程函数写入更新时间和创建时间](https://blog.csdn.net/qietingfengsong/article/details/124097229)\n\n[APIJSON（一：综述）](https://blog.csdn.net/qq_50861917/article/details/120556168)\n\n[APIJSON 代码分析（三：demo主体代码）](https://blog.csdn.net/qq_50861917/article/details/120751630)\n\n[APIJSON 代码分析（二）AbstractParser类(解析器)](https://blog.csdn.net/weixin_45767055/article/details/120815927)\n\n[APIJSON 代码分析（四：AbstractObjectParser源码阅读）](https://blog.csdn.net/qq_50861917/article/details/120896381)\n\n[APIJSON 代码分析 AbstractSQLConfig 第二篇](https://blog.csdn.net/csascscascd/article/details/120684889)\n\n[APIJSON 代码分析（六）APIJSON—Verifier检查类](https://blog.csdn.net/weixin_45767055/article/details/121321731)\n\n[APIJSON 代码分析（四）AbstractSQLExecutor—SQL执行器](https://blog.csdn.net/weixin_45767055/article/details/121069887)\n\n[APIJSON使用](https://juejin.cn/post/7148253873478565902)\n\n[apijson 初探](https://www.cnblogs.com/x3d/p/apijson-lowcode.html)\n\n[APIJSON使用介绍](http://api.flyrise.cn:9099/docs/open-docs//1459)\n\n[MassCMS With APIJSON最佳实践](https://zhuanlan.zhihu.com/p/655826966)\n\n[APIJSON语法使用，超详细](https://blog.csdn.net/qq_36565607/article/details/139167040)\n\n[wend看源码-ORM-APIJSON](https://itwend.blog.csdn.net/article/details/143980281)\n\n[APIJSON – The No-Code API Revolution That Puts Developers in the Fast Lane](https://medevel.com/apijson)\n\n[APIJSON：17.4k Star！腾讯开源的零代码接口与文档协议及ORM库](https://mp.weixin.qq.com/s/gr84DmWKs4O6lcoT-iaV5w)\n\n[APIJSON腾讯开源的后端开发神器！！！](https://cloud.tencent.com/developer/article/2372220)\n\n[apijson 快速上手](https://blog.csdn.net/qq_16381291/article/details/147110737)\n\n[APIJSON快速入门-零后端代码，接口所见即所得](https://www.toutiao.com/article/7503844050689376783)\n\n[腾讯开源！零代码，全自动万能API接口](https://mp.weixin.qq.com/s/WWndAa68BqBfflWgL5592A)\n\n[APIJSON项目实战教程：零代码实现高效JSON接口开发](https://blog.csdn.net/gitblog_00682/article/details/148375065)\n\n[springboot整合APIJSON——零代码万能通用 API（附源码）](https://blog.csdn.net/longzhutengyue/article/details/150579233)\n\n[APIJSON：重新定义后端开发体验的零代码ORM框架](https://blog.csdn.net/gitblog_01177/article/details/155216163)\n\n[API自动生成这么爽？实测腾讯APIJSON，零代码就能玩转后端接口！](https://mp.weixin.qq.com/s/DmMIGHHcZ783KobGecMxGg)\n\n[腾讯开源的 APIJSON：后端接口不用写了？](https://mp.weixin.qq.com/s/zhkfG4AQEsg0N87lhStwvw)\n\n[3分钟掌握APIJSON搜索黑科技：从模糊匹配到智能检索](https://blog.csdn.net/gitblog_00009/article/details/152403741)\n\n### 生态项目\n[APIJSON-Demo](https://github.com/APIJSON/APIJSON-Demo) APIJSON 各种语言、各种框架 的 使用示例项目、上手文档、测试数据 SQL 文件 等\n\n[apijson-orm](https://github.com/APIJSON/apijson-orm) APIJSON ORM 库，可通过 Maven, Gradle 等远程依赖\n\n[apijson-framework](https://github.com/APIJSON/apijson-framework) APIJSON 服务端框架，通过数据库表配置角色权限、参数校验等，简化使用\n\n[apijson-router](https://github.com/APIJSON/apijson-router) APIJSON 的路由插件，可控地对公网暴露类 RESTful 简单接口，内部转成 APIJSON 格式请求来执行\n  \n[apijson-column](https://github.com/APIJSON/apijson-column) APIJSON 的字段插件，支持 字段名映射 和 !key 反选字段\n\n[apijson-jackson](https://github.com/APIJSON/apijson-jackson) APIJSON 的 jackson 插件，简化使用\n\n[apijson-fastjson2](https://github.com/APIJSON/apijson-fastjson2) APIJSON 的 fastjson2 插件，简化使用\n\n[apijson-gson](https://github.com/APIJSON/apijson-gson) APIJSON 的 gson 插件，简化使用\n\n[apijson-milvus](https://github.com/APIJSON/apijson-milvus) APIJSON 的 Milvus AI 向量数据库插件\n\n[apijson-influxdb](https://github.com/APIJSON/apijson-influxdb) APIJSON 的 InfluxDB 物联网时序数据库插件\n\n[apijson-mongodb](https://github.com/APIJSON/apijson-mongodb) APIJSON 的 MongoDB NoSQL 数据库插件\n\n[apijson-cassandra](https://github.com/APIJSON/apijson-cassandra) APIJSON 的 Cassandra NoSQL 数据库插件\n\n[APIAuto](https://github.com/TommyLemon/APIAuto) ☔ 敏捷开发最强大易用的接口工具，零代码测试与 AI 问答、生成代码与静态检查、生成文档与光标悬浮注释，腾讯、SHEIN、传音 等使用\n\n[CVAuto](https://github.com/TommyLemon/CVAuto) 👁 零代码零标注 CV AI 自动化测试平台 🚀 免除大量人工画框和打标签等，直接快速测试 CV 计算机视觉 AI 图像识别算法\n\n[UnitAuto](https://github.com/TommyLemon/UnitAuto) 最先进、最省事、ROI 最高的单元测试，机器学习 零代码、全方位、自动化 测试 方法/函数，用户包含腾讯、快手、某 500 强巨头等\n\n[SQLAuto](https://github.com/TommyLemon/SQLAuto) 智能零代码自动化测试 SQL 数据库工具，任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据\n\n[UIGO](https://github.com/TommyLemon/UIGO) 📱 零代码快准稳 UI 智能录制回放平台 🚀 3 像素内自动精准定位，2 毫秒内自动精准等待，用户包含腾讯，微信团队邀请分享\n\n[apijson-doc](https://github.com/vincentCheng/apijson-doc) APIJSON 官方文档，提供排版清晰、搜索方便的文档内容展示，包括设计规范、图文教程等\n\n[APIJSONdocs](https://github.com/ruoranw/APIJSONdocs) APIJSON 英文文档，提供排版清晰的文档内容展示，包括详细介绍、设计规范、使用方式等\n\n[apijson.org](https://github.com/APIJSON/apijson.org) APIJSON 官方网站，提供 APIJSON 的 功能简介、登记用户、作者与贡献者、相关链接 等\n\n[APIJSON.NET](https://github.com/liaozb/APIJSON.NET) C# 版 APIJSON ，支持 MySQL, PostgreSQL, SQL Server, Oracle, SQLite\n\n[apijson-go](https://github.com/glennliao/apijson-go) Go 版 APIJSON ， 基于Go(>=1.18) + GoFrame2, 支持查询、单表增删改、权限管理等\n  \n[apijson-go](https://gitee.com/tiangao/apijson-go) Go 版 APIJSON ，支持单表查询、数组查询、多表一对一关联查询、多表一对多关联查询 等\n\n[apijson-hyperf](https://github.com/kvnZero/hyperf-APIJSON.git) PHP 版 APIJSON，基于 Hyperf 支持 MySQL\n\n[APIJSON-php](https://github.com/xianglong111/APIJSON-php) PHP 版 APIJSON，基于 ThinkPHP，支持 MySQL, PostgreSQL, SQL Server, Oracle 等\n\n[apijson-php](https://github.com/qq547057827/apijson-php) PHP 版 APIJSON，基于 ThinkPHP，支持 MySQL, PostgreSQL, SQL Server, Oracle 等\n\n[apijson-node](https://github.com/kevinaskin/apijson-node) 字节跳动工程师开源的 Node.ts 版 APIJSON，提供 nestjs 和 typeorm 的 Demo 及后台管理\n\n[uliweb-apijson](https://github.com/zhangchunlin/uliweb-apijson) Python 版 APIJSON，支持 MySQL, PostgreSQL, SQL Server, Oracle, SQLite 等\n\n[apijson-rust](https://gitee.com/APIJSON/panda-base) APIJSON 的 Rust 版，一个优雅、高性能的 Rust 多数据源管理系统，支持 MySQL 和 PostgreSQL\n\n[APIJSONParser](https://github.com/Zerounary/APIJSONParser) 第三方 APIJSON 解析器，将 JSON 动态解析成 SQL\n\n[FfApiJson](https://gitee.com/own_3_0/ff-api-json) 用 JSON 格式直接生成 SQL，借鉴 APIJSON 支持多数据源\n\n[APIJSON-ToDo-Demo](https://github.com/jerrylususu/apijson_todo_demo) 一个简单的 todo 示例项目，精简数据，简化上手流程，带自定义鉴权逻辑 \n\n[apijson-learn](https://github.com/rainboy-learn/apijson-learn) APIJSON 学习笔记和源码解析\n\n[apijson-practice](https://github.com/vcoolwind/apijson-practice) BAT 技术专家开源的 APIJSON 参数校验注解 Library 及相关 Demo\n\n[apijson-db2](https://github.com/andream7/apijson-db2) 微软工程师接入 IBM 数据库 DB2 的 APIJSON 使用 Demo\n\n[APIJSONDemo](https://github.com/qiujunlin/APIJSONDemo) 字节跳动工程师接入 ClickHouse 的 APIJSON 使用 Demo\n  \n[APIJSONDemo_ClickHouse](https://github.com/chenyanlann/APIJSONDemo_ClickHouse) APIJSON + SpringBoot 连接 ClickHouse 使用的 Demo\n\n[APIJSONBoot_Hive](https://github.com/chenyanlann/APIJSONBoot_Hive) APIJSON + SpringBoot 连接 Hive 使用的 Demo\n\n[apijson-sample](https://gitee.com/greyzeng/apijson-sample) APIJSON 简单使用 Demo 及教程\n\n[apijson-examples](https://gitee.com/drone/apijson-examples) APIJSON 的前端、业务后端、管理后端 Demo\n\n[apijson-ruoyi](https://github.com/daodol/apijson-ruoyi) APIJSON 和 RuoYi 框架整合，实现零代码生成页面模板接口，在线维护 APIJSON 数据库配置等\n\n[light4j](https://github.com/xlongwei/light4j) 整合 APIJSON 和微服务框架 light-4j 的 Demo，同时接入了 Redis\n\n[SpringServer1.2-APIJSON](https://github.com/Airforce-1/SpringServer1.2-APIJSON) 智慧党建服务器端，提供 上传 和 下载 文件的接口\n\n[apijson_template](https://github.com/abliger/apijson_template) APIJSON Java 模版，使用 gradle 管理依赖和构建应用\n  \n[api-json-demo](https://gitee.com/hxdwd/api-json-demo) 基于 APIJSON，实现低代码写 CURD 代码，代替传统 ORM 框架，适配 Oracle 事务\n\n[ApiJsonByJFinal](https://gitee.com/zhiyuexin/ApiJsonByJFinal) 整合 APIJSON 和 JFinal 的 Demo\n\n[apijson-go-demo](https://github.com/glennliao/apijson-go-demo) apijson-go demos，提供 3 个从简单到复杂的不同场景 Demo\n\n[apijson-builder](https://github.com/pengxianggui/apijson-builder) 一个方便为 APIJSON 构建 RESTful 请求的 JavaScript 库\n\n[apijson-go-ui](https://github.com/glennliao/apijson-go-ui) apijson-go UI 界面配置, 支持权限管理、请求规则配置等\n\n[AbsGrade](https://github.com/APIJSON/AbsGrade) 列表级联算法，支持微信朋友圈单层评论、QQ空间双层评论、百度网盘多层(无限层)文件夹等\n\n[APIJSON-Android-RxJava](https://github.com/TommyLemon/APIJSON-Android-RxJava) 仿微信朋友圈动态实战项目，ZBLibrary(UI) + APIJSON(HTTP) + RxJava(Data)\n\n[Android-ZBLibrary](https://github.com/TommyLemon/Android-ZBLibrary) Android MVP 快速开发框架，Demo 全面，注释详细，使用简单，代码严谨\n  \n[apijson-dynamic-datasource](https://github.com/wb04307201/apijson-dynamic-datasource) 基于APIJSON，动态切换数据源、同一数据源批量操作事务一致性DEMO\n  \n[xyerp](https://gitee.com/yinjg1997/xyerp) 基于ApiJson的低代码ERP\n  \n[quick-boot](https://github.com/csx-bill/quick-boot) 基于 Spring Cloud 2022、Spring Boot 3、AMIS 和 APIJSON 的低代码系统。\n\n[apijson-query-spring-boot-starter](https://gitee.com/mingbaobaba/apijson-query-spring-boot-starter) 一个快速构建 APIJSON 查询条件的插件\n\n[apijson-builder](https://github.com/yeli19950109/apijson-builder) 简单包装 APIJSON，相比直接构造查询 JSON 更好记，ts 编写，调整了一些参数和使用方式\n\n[lanmuc](https://gitee.com/element-admin/lanmuc) 后端低代码生产接口的平台，兼容配置式接口和编写式接口，可做到快速生产接口，上线项目\n\n[review_plan](https://gitee.com/PPXcodeTry/review_plan) 复习提醒Web版（Java技术练习项目）\n\n[apijson-nutz](https://github.com/vincent109/apijson-nutz) APIJSON + Nutz 框架 + NutzBoot 的 Demo\n\n[apijson-spring-boot](https://gitee.com/yunjiao-source/apijson-spring-boot) Springboot3 for APIJSON，用 YAML 简化代码配置\n\n感谢热心的作者们的贡献，点 ⭐Star 支持下他们吧~\n\n  \n### 腾讯犀牛鸟开源人才培养计划\nhttps://github.com/Tencent/APIJSON/issues/229\n\n\n####  qiujunlin **2.接入 presto/hive/clickhouse/db2 任意一个**\n\nAPIJSON 接入 clickhouse 使用demo <br />\nhttps://github.com/qiujunlin/APIJSONDemo\n\n#### zhangshukun 2.接入 presto/hive/clickhouse/db2 任意一个 \nAPIJSON-Demo接入db2 <br />\nhttps://github.com/andream7/apijson-db2\n\n#### hanxu 1.完善入门介绍视频 \n重构 APIJSON 文档 <br /> \nhttps://hanxu2018.github.io/APIJSON-DOC/ <br /> \n文档源码  <br /> \nhttps://github.com/HANXU2018/APIJSON-DOC  <br /> \n配套评论区 apijson-doc-Comment  <br /> \nhttps://github.com/HANXU2018/apijson-doc-Comment\n\n#### chenyanlan 2.接入 presto/hive/clickhouse/db2 任意一个 \nAPIJSON + SpringBoot连接ClickHouse使用的Demo <br /> \nhttps://github.com/chenyanlann/APIJSONDemo_ClickHouse\n\n#### zhaoqiming 1.完善入门介绍视频  \nAPIJSON 后端教程（1）：简介\nhttps://www.bilibili.com/video/BV1vL411W7yd\n\nAPIJSON 后端教程（2）：数据库 \nhttps://www.bilibili.com/video/BV1eB4y1N77s\n\nAPIJSON 后端教程（3）：Demo\nhttps://www.bilibili.com/video/BV1FX4y1c7ug\n\nAPIJSON 后端教程（4）：Boot\nhttps://www.bilibili.com/video/BV18h411z7FK\n\nAPIJSON 后端教程（5）：Final\nhttps://www.bilibili.com/video/BV1GM4y1N7XJ\n\nAPIJSON 后端教程（6）：uliweb_apijson\nhttps://www.bilibili.com/video/BV1yb4y1S79v/\n\nAPIJSON 后端教程（7）：问题答疑\nhttps://www.bilibili.com/video/BV1dQ4y1h7Df\n  \nAPIJSON配套文档：\nhttps://github.com/kenlig/apijsondocs\n\n#### huwen 2.接入 presto/hive/clickhouse/db2 任意一个\nAPIJSON-Demo 接入presto\nhttps://github.com/hclown9804/APIJSONDemo_presto\n\n#### zhanghaoling 1.完善入门介绍视频\nAPIJSON结合已有项目，简化开发流程\nhttps://github.com/haolingzhang1/APIJson--demo\n\n说明文档\nhttps://github.com/haolingzhang1/APIJson--demo/tree/main/APIJson集成项目说明\n  \n(1)官方demo\nhttps://github.com/haolingzhang1/APIJson--demo/blob/main/APIJson集成项目说明/APIJson集成现有项目（1）-%20官方demo.pdf\n  \n(2)单表配置\nhttps://github.com/haolingzhang1/APIJson--demo/blob/main/APIJson集成项目说明/APIJson集成现有项目（2）-%20单表配置.pdf \n  \n#### zhoukaile 1.完善入门介绍视频\n\n视频链接：https://www.bilibili.com/video/BV1Uh411z7kZ/\n\n文档链接：https://gitee.com/funkiz/apijson_camp\n\n#### lintao 1.完善入门介绍视频\n\nAPIJSON 上手教程：https://www.bilibili.com/video/BV1Pq4y1n7rJ\n\n### 持续更新\n\nhttps://github.com/Tencent/APIJSON/commits/master\n\n### 工蜂主页\nhttps://git.code.tencent.com/Tencent_Open_Source/APIJSON\n\n### 码云主页\nhttps://gitee.com/Tencent/APIJSON\n"
  },
  {
    "path": "README-extend.md",
    "content": "# 功能点\n\n### 一个json(事务)同时支持新增、修改、删除、查询、别名\n\nhttps://github.com/Tencent/APIJSON/issues/468\n\n#### 使用说明\n\njson 支持多种方式定义 method\n\n#### 第一种:\n\n\"@post\",\"@put\",\"@delete\",\"@head\",\"@get\",\"@gets\",\"@head\",\"@heads\"\n\n\"@post\": \"Moment,Comment[]\" , 值为 String 或 JSONObject 格式,  为 String 时每个 value = key，为 JSONObject 时：\n```json\n\"@post\": { \n   \"Moment\": \"Moment\", // 只指定 tag，为 \"\" 则和 key 一致\n   \"Comment[]\": { // 同时指定多个全局关键词\n      \"tag\": \"Comment[]\",\n      \"version\": 2\n      // 其它全局关键词\n   }\n}\n```\n\n需要保证每个key唯一, 唯一判断标准:\n\nkey = Moment \n\nkey= Moment[] \n\n会判断为相同key. 请通过别名区分, 别名格式: Sys_user_role:sur  xxx表名:别名\n\n```\n{\n   \"@post\": \"Moment,Comment:cArray[],User:u\", // 分发到 POST 请求对应的解析处理\n   \"Moment\": {\n     // TODO 其它字段\n   },\n   \"Comment:cArray[]\": [\n      {\n        // TODO 其它字段\n      }\n   ],\n   \"@get\": \"User\", // 分发到 GET 请求对应的解析处理\n   \"User:u\": {\n     // TODO 其它字段\n   },\n   \"Privacy\": { // 按 URL 对应的默认方法处理\n     // TODO 其它字段\n   }\n}\n\n对于没有显式声明操作方法的，直接用 URL(/get, /post 等) 对应的默认操作方法\n\n```\n\n#### 第二种: @Deprecated 即将弃用，请使用第一种\n\n对象内定义\"@method\": \"GET\", value大写\n\n```\n{\n    \"sql@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n            \"@column\": \"id\",\n            \"role_name\": \"角色1\"\n        }\n    },\n    \"Sys_user_role:sur[]\": {\n    \t\"@method\": \"GET\",\n        \"Sys_user_role\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"Sys_role_permission:srp[]\": {\n    \t\"@method\": \"GET\",\n        \"Sys_role_permission\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"@explain\": true\n}\n```\n\n#### 解析顺序\n\n1) 对象内 \"@method\"\n2) \"@post\",\"@put\",\"@delete\"\n3) 对于没有显式声明操作方法的，直接用 URL(/get, /post 等) 对应的默认操作方法\n\n#### tag自动生成规则\n\n/**\n * { \"xxx:aa\":{ \"@tag\": \"\" }}\n * 生成规则:\n * 1、@tag存在,tag=@tag\n * 2、@tag不存在\n * 1)、存在别名\n * key=对象: tag=key去除别名\n * key=数组: tag=key去除别名 + []\n * 2)、不存在别名\n * tag=key\n * tag=key + []\n */\n\n\n![image](https://user-images.githubusercontent.com/12228225/204079184-06dd08a7-95a3-4a46-8e05-f062fa406847.png)\n\n\n#### 建议\n\n1. 一个json包含不同操作方法, url method 使用 /post, /put\n2. value为JSONArray, 建议通过\"@post\" 方式配置, 如果没有配置,执行 3\n\n#### Request表 配置说明\n\n这只是我的配置仅供参考, 后续 测试会用到:\n\n```\n单条新增:\nPOST   User_address     {\"MUST\":\"addr\",\"UPDATE\": {\"@role\": \"OWNER,ADMIN\",\"childFunTest-()\": \"childFunTest(addr)\"}, \"REFUSE\": \"id\"}\n批量新增:\nPOST User_address[]  {\"User_address[]\": [{\"MUST\":\"addr\",\"REFUSE\": \"id\"}], \"UPDATE\": {\"@role\": \"OWNER,ADMIN\",\"childFunTest-()\": \"childFunTest(addr)\"}}\n单条修改:\nPUT User_address   {\"User_address\":{ \"MUST\":\"id\",\"REFUSE\": \"userId\", \"UPDATE\": {\"@role\": \"OWNER,ADMIN\",\"childFunTest-()\": \"childFunTest(addr)\"}} }\n批量修改:\nPUT User_address[]  {\"User_address[]\": [{\"MUST\": \"id\",\"REFUSE\": \"userId\"}], \"UPDATE\": {\"@role\": \"OWNER,ADMIN\"}}\n删除:\nDELETE User_address   {\"User_address\":{ \"MUST\":\"id{}\",\"REFUSE\": \"!\", \"INSERT\": {\"@role\": \"OWNER,ADMIN\"}} }\n\n```\n![image](https://user-images.githubusercontent.com/12228225/204079438-8f352496-4b73-4b72-88c0-914894335074.png)\n\n\n\n### 别名\n\n格式: \n\nSys_user_role:sur  xxx表名:别名\n\nComment:cArray[]\n\n#### 实现思路\n\n当时参考了作者的示例: 注册流程. 看到绕过校验, 可以将多条json语句组装在一起, 批量执行. 于是就想如何实现一个json支持不同操作方法,并支持事物. \n\n通过分析源码, 熟悉了校验流程、json解析执行流程、json解析生成sql语句流程、一些兼容、校验规则等\n\n经过和作者讨论, 很感谢作者提供了相关解决方案和思路. 逐步理解了apijson的设计原理和框架结构.\n\n一个json(事务)同时支持新增、修改、删除、查询、别名, 实现思路如下:\n\n1、校验模块\n\n将json解析成对象、临时变量、子查询、别名、tag等\n\n并将method 添加到 json对象属性中.\n\n```json\n\"Sys_role\": {\n    \"@method\": \"PUT\",\n    \"id\": \"6aedce0d-2a29-4fbe-aeed-0ba935ca6b41\",\n    \"id{}@\": \"sql\",\n    \"role_code\": \"code-subrange-4\",\n    \"role_name\": \"角色-subrange-4\"\n}\n```\n\n2、对象解析\n\n用对象属性@method , 替换 Parser 的 method\n\n3、事物支持\n\n### 后续优化建议\n\n1、独立定义一个url method, 通过解析不同method执行不同流程\n\n和已有method区分开,避免歧义\n\n2、最外层新增传参 \"transaction\": true 来指定开启事务\n目前是url put、post来控制开启事物, 以及提交的时候 在 AbstractParser onCommit 判断 transactionIsolation (4 : 开启事物, 0: 非事物请求) \n\n![image](https://user-images.githubusercontent.com/12228225/204079532-26d9cd4b-d2d7-4c73-9f78-f425bbbcf623.png)\n\n详细实现请参见: https://github.com/Tencent/APIJSON/issues/468\n\n3、完善 \"[@Explain](https://github.com/Explain)\"\n\n如果没有执行计划,则返回sql语句. 能够在 reponse返回值中, 看到json中执行的每条sql,方便排错\n\n![image](https://user-images.githubusercontent.com/12228225/204079543-be464f67-a80f-4a33-87ea-d1870908e642.png)\n\n4、@version支持\n\n定义不同场景的 新增、修改、删除等执行规则. 请通过version版本区分\n\nRequest表是通过tag、method、version来保证唯一.\n\n![image](https://user-images.githubusercontent.com/12228225/204079562-00449c38-42b1-4d9c-b562-2d56c77e6218.png)\n\n5、前置函数\n\n前置函数能够将json语句, 加入到 当前事物中.\n\n例如:  像数组一样,解析成每一条语句去执行.\n\n### mysql8 with-as表达式\n\n#### 前提条件\n\n1、mysql版本: 8+\n\n2、mysql-connector-java: 8.0.31\n\n版本支持 with-as即可\n\n3、druid: 1.2.15\n\n版本支持 with-as即可\n\n4、去掉 durid wall配置\n\ndelete子查询,  druid wall 拦截器报错 sql injection violation\n\n![image](https://user-images.githubusercontent.com/12228225/204079572-19a4f50c-3bf3-4f9e-9677-6aa191276fef.png)\n\n#### 测试案例\n\n#### 查询单个range ref引用\n\n```\n// 测试 mysql8 with as表达式\n// 用户表\n// 用户角色表\n// 角色表\n// 示例一 单个range ref引用\n{\n\t\"sql@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n          \"@column\": \"id\",\n          \"role_name\": \"角色1\"\n        }\n    },\n    \"Sys_user_role:sur[]\": {\n    \t\"@method\": \"GET\",\n        \"Sys_user_role\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"Sys_role_permission:srp[]\": {\n    \t\"@method\": \"GET\",\n        \"Sys_role_permission\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"@explain\": true\n}  \n\n// 第二种写法\n{\n\t\"@get\": [\"sql@\",\"Sys_user_role:sur[]\",\"Sys_role_permission:srp[]\"],\n\t\"sql@\": {\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n          \"@column\": \"id\",\n          \"role_name\": \"角色1\"\n        }\n    },\n    \"Sys_user_role:sur[]\": {\n        \"Sys_user_role\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"Sys_role_permission:srp[]\": {\n        \"Sys_role_permission\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"@explain\": true\n} \n```\n\nmysql8执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079581-bf835db2-30ae-4265-bda2-ebf34c0d9e77.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079594-3ebc73a0-836e-4073-9aa4-acb665fe8d52.png)\n\n\n#### 查询多个range ref引用\n\n```\n{\n    \"sql@\": {\n        \"@method\": \"GET\", \n        \"with\": true, \n        \"from\": \"Sys_role\", \n        \"Sys_role\": {\n            \"@column\": \"id\", \n            \"role_name\": \"角色1\"\n        }\n    }, \n    \"sql_user@\": {\n        \"@method\": \"GET\", \n        \"with\": true, \n        \"from\": \"Sys_user\", \n        \"Sys_user\": {\n            \"@column\": \"id\", \n            \"id\": \"f0894db2-6940-4d89-a5b2-4405d0ad0c8f\"\n        }\n    }, \n    \"Sys_user_role:sur[]\": {\n        \"@method\": \"GET\", \n        \"Sys_user_role\": {\n            \"role_id{}@\": \"sql\", \n            \"user_id{}@\": \"sql_user\"\n        }\n    }, \n    \"Sys_role_permission:srp[]\": {\n        \"@method\": \"GET\", \n        \"Sys_role_permission\": {\n            \"role_id{}@\": \"sql\"\n        }\n    }, \n    \"@explain\": true\n}\n```\n\nmysql8执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079603-2ba224a3-3174-491a-a71b-7656c97d0146.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079611-155f6a33-ad56-4d03-8e5d-6f44c3649051.png)\n\n#### delete子查询\n\n```j s\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role_permission\",\n        \"Sys_role_permission\": {\n          \"@column\": \"id\",\n\t\t  \"role_id{}\": [\"e7129d5f-b07e-4996-9965-9528e370a393\"]\n        }\n    },\n    \"Sys_role_permission\": {\n    \t\"@method\": \"DELETE\",\n        \"id{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n\n```\n\n![image](https://user-images.githubusercontent.com/12228225/204079615-25185be5-a296-488f-9a13-98fb2b99a9d5.png)\n\nmysql8执行sql语句:\n\n```\nWITH  `sql` AS (SELECT `id` FROM `housekeeping`.`Sys_role_permission` WHERE  (  (`role_id` IN ('68877ee9-4cf4-4f32-86e6-16c505ca3b21'))  ) ) DELETE FROM `housekeeping`.`Sys_role_permission` WHERE  (  (`id` IN ( SELECT * FROM `sql`) )  ) \n\nPlain Text\n```\n\nmysql5.7执行结果:\n\n```\nDELETE FROM `housekeeping`.`Sys_role_permission` WHERE  (  (`id` IN ( SELECT * FROM (SELECT `id` FROM `housekeeping`.`Sys_role_permission` WHERE  (  (`role_id` IN ('20d337bb-9886-455f-8dce-f1cadab0ec4f'))  ) ) AS `sql`) )  ) \n\n\n\nPlain Text\n```\n\n#### update子查询\n\n```\n{\n   \"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role_permission\",\n        \"Sys_role_permission\": {\n          \"@column\": \"role_id\",\n\t\t  \"id{}\": [\"6aedce0d-2a29-4fbe-aeed-0ba935ca6b41\"]\n        }\n    },\n    \"Sys_role\": {\n    \t\"@method\": \"PUT\",\n\t\t\"id\": \"6aedce0d-2a29-4fbe-aeed-0ba935ca6b41\",\n        \"id{}@\": \"sql\",\n        \"role_code\": \"code-subrange-4\",\n        \"role_name\": \"角色-subrange-4\"\n    },\n    \"@explain\": true\n}\n\n第二种写法\n{\n\t\"@get\": [\"sql@\"],\n    \"sql@\": {\n        \"with\": true,\n        \"from\": \"Sys_role_permission\",\n        \"Sys_role_permission\": {\n          \"@column\": \"role_id\",\n\t\t  \"id{}\": [\"c95ef2d6-bf14-42b0-bb87-038cee8c78f1\"]\n        }\n    },\n    \"@put\": [\"Sys_role\"],\n    \"Sys_role\": {\n\t\t\"id\": \"0bb92d96-8ca6-469e-91e8-60308ce5b835\",\n        \"id{}@\": \"sql\",\n        \"role_code\": \"code-subrange-4\",\n        \"role_name\": \"角色-subrange-4\"\n    },\n    \"@explain\": true\n}\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079628-8536b4be-8078-42a5-b3f7-460159372a8a.png)\n\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079633-df9175bc-703f-4997-95f6-85bbc1134b0b.png)\n\n#### GETS 单条子查询\n\n会执行校验流程\n\n```\nhttp://localhost:8675/lowCodePlatform/forms/api/gets\n\n{\n\t\"sql@\": {\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"role_id\",\n\t\t  \"user_id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_role[]\": {\n        \"Sys_role\": {\n            \"id{}@\": \"sql\"\n        },\n        \"page\": 0,\n        \"count\": 10,\n        \"query\": 2\n    },\n    \"tag\":\"Sys_role[]\",\n    \"total@\": \"/Sys_role[]/total\",\n    \"@explain\": true\n}\n\n第二种写法\n{\n\t\"@gets\": [\"sql@\",\"Sys_role[]\"],\n\t\"sql@\": {\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"role_id\",\n\t\t  \"user_id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_role[]\": {\n        \"Sys_role\": {\n            \"id{}@\": \"sql\"\n        },\n        \"page\": 0,\n        \"count\": 10,\n        \"query\": 2\n    },\n    \"tag\":\"Sys_role[]\",\n    \"total@\": \"/Sys_role[]/total\",\n    \"@explain\": true\n}\n\n```\n\nAccess、Request需要配置鉴权信息:\n\n![image](https://user-images.githubusercontent.com/12228225/204079649-510a047b-2b8e-44d2-a32a-f6ea0e7f6a74.png)\n\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079657-6e62872a-2f29-478e-a29b-bcb0a92781a6.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079878-a9885b86-5a44-4ba2-b837-66adc43b07d3.png)\n\n#### GETS多条子查询\n\n会执行校验流程\n\n```\nhttp://localhost:8675/lowCodePlatform/forms/api/gets\n\n{\n\t\"sql@\": {\n    \t\"@method\": \"GETS\",\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n          \"@column\": \"id\",\n          \"role_name\": \"超级管理员\"\n        }\n    },\n    \"sql_user@\": {\n    \t\"@method\": \"GETS\",\n        \"with\": true,\n        \"from\": \"Sys_user\",\n        \"Sys_user\": {\n          \"@column\": \"id\",\n          \"id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_user_role:sur[]\": {\n    \t\"@method\": \"GETS\",\n        \"Sys_user_role\": {\n            \"role_id{}@\": \"sql\",\n            \"user_id{}@\": \"sql_user\"\n        }\n    },\n    \"Sys_role_permission:srp[]\": {\n    \t\"@method\": \"GETS\",\n        \"Sys_role_permission\": {\n            \"role_id{}@\": \"sql\"\n        }\n    },\n    \"@explain\": true\n}\n\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079892-bc71eb65-cfbd-4c3c-bda9-4b31902058ba.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079897-521a763f-bb08-44af-92c6-5e4117fe9d33.png)\n\n#### head 单个子查询\n\n普通获取数量, get/head不执行校验流程\n\n```\nhttp://localhost:8675/lowCodePlatform/forms/api/head\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"role_id\",\n\t\t  \"user_id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_role\": {\n    \t\"@method\": \"head\",\n        \"id{}@\": \"sql\"\n    },\n    \"@explain\": true\n}\n\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079903-e397a78a-1849-4678-ac41-0611165a1de1.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079908-1efb5b28-889d-4d9b-b4f9-5092925888c9.png)\n\n#### head 多个子查询\n\n普通获取数量, get/head不执行校验流程\n\n```\n{\n\t\"sql@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n          \"@column\": \"id\",\n          \"role_name\": \"超级管理员\"\n        }\n    },\n    \"sql_user@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user\",\n        \"Sys_user\": {\n          \"@column\": \"id\",\n          \"id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_user_role\": {\n        \"@method\": \"HEAD\",\n        \"role_id{}@\": \"sql\",\n            \"user_id{}@\": \"sql_user\"\n    },\n    \"Sys_role_permission\": {\n        \"@method\": \"HEAD\",\n        \"role_id{}@\": \"sql\"\n    },\n    \"@explain\": true\n}\n\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079919-5fba8f87-56d8-4d7d-b457-4a2505f27d1e.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079932-1e040caf-57fd-45a7-afa5-b26bdce83fba.png)\n\n#### heads 单个子查询\n\n普通获取数量\n\n会执行校验流程, Access、Request需要配置鉴权信息:\n![image](https://user-images.githubusercontent.com/12228225/204079942-d790a3c0-eb46-4512-bb58-45a16894608a.png)\n\n```\nhttp://localhost:8675/lowCodePlatform/forms/api/heads\n\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"role_id\",\n\t\t  \"user_id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_role\": {\n    \t\"@method\": \"heads\",\n        \"id{}@\": \"sql\"\n    },\n    \"@explain\": true\n}\n\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079952-976fa9b6-4a11-40ad-a2c7-6f901b186670.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079959-6bf95b45-5f35-474e-b428-b51bcb5b500d.png)\n\n#### heads 多个子查询\n\n会执行校验流程, Access、Request需要配置鉴权信息:\n\n![image](https://user-images.githubusercontent.com/12228225/204079967-a48f4f50-6e6b-476b-a281-b072ef8a352d.png)\n\n普通获取数量\n\n```\n{\n\t\"sql@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role\",\n        \"Sys_role\": {\n          \"@column\": \"id\",\n          \"role_name\": \"超级管理员\"\n        }\n    },\n    \"sql_user@\": {\n    \t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user\",\n        \"Sys_user\": {\n          \"@column\": \"id\",\n          \"id\": \"4732209c-5785-4827-b532-5092f154fd94\"\n        }\n    },\n    \"Sys_user_role\": {\n        \"@method\": \"HEADS\",\n        \"role_id{}@\": \"sql\",\n        \"user_id{}@\": \"sql_user\"\n    },\n    \"Sys_role_permission\": {\n        \"@method\": \"HEADS\",\n        \"role_id{}@\": \"sql\"\n    },\n    \"@explain\": true\n}\n\n```\n\nmysql8执行sql语句:\n\n![image](https://user-images.githubusercontent.com/12228225/204079980-c93ef595-0c4b-42a7-a3b3-1e7402d3cb13.png)\n\nmysql5.7执行结果:\n\n![image](https://user-images.githubusercontent.com/12228225/204079987-878d5937-3f42-4f59-93dc-b5a840f5548c.png)\n\n### delete、put 支持子查询\n\nhttps://github.com/Tencent/APIJSON/issues/471\n\n静态变量做全局处理，特殊接口用 Operation.MUST id/id{}/id{}@ 做自定义处理。\n\n之所以默认必传，是因为安全意识不够、编码粗心大意的人太多了，所以要有一个底线保障，尽可能避免安全隐患。\n\n1、全局配置 为 PUT, DELETE 强制要求必须有 id/id{}/id{}@ 条件\n\nAbstractVerifier.IS_UPDATE_MUST_HAVE_ID_CONDITION = true; // true: 必须有\n\n![image](https://user-images.githubusercontent.com/12228225/204080001-eef4ee65-0ad0-4a41-93ba-9b16cd1c2e0e.png)\n\n2、细粒度控制\n\n![image](https://user-images.githubusercontent.com/12228225/204080012-f7d781e9-0a53-461f-84db-3d6ecb167e20.png)\n\n#### 使用说明\n\n```\n// 条件删除\n```json\n{\n    \"User:del\": {\n        \"username\": \"test3\"\n    },\n    \"tag\": \"User\",\n    \"explain\": true\n}\n```\n\n// 引用id{}@删除\n```json\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"user_id\",\n\t\t  \"role_id{}\": [\"023e1880-c0d4-4e7c-ae6c-7703199c2daf\"]\n        }\n    },\n    \"Sys_user:aa\": {\n    \t\"@method\": \"DELETE\",\n        \"id{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n// 子查询条件删除\nhttp://localhost:8675/lowCodePlatform/forms/api/delete\n```json\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"User\",\n        \"User\": {\n          \"@column\": \"username\",\n\t\t  \"username\": \"test-3\"\n        }\n    },\n    \"User\": {\n        \"username{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n第二种写法:\n```json\n{\n\t\"@get\": [\"sql@\"],\n\t\"sql@\": {\n        \"with\": true,\n        \"from\": \"User\",\n        \"User\": {\n          \"@column\": \"username\",\n\t\t  \"username\": \"test4\"\n        }\n    },\n    \"User\": {\n        \"username{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n\n\n开启id删除, 删除失败:\n\n```json\n{\n    \"@get\": [\"sql@\"],\n    \"sql@\": {\n        \"with\": true,\n        \"from\": \"User\",\n        \"User\": {\n            \"@column\": \"username\",\n            \"username\": \"test4\"\n        }\n    },\n    \"User\": {\n        \"username{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n![image](https://user-images.githubusercontent.com/12228225/204080043-6614457c-a0ed-45b3-a26a-e75126dbb486.png)\n\n开启id删除、id引用 删除成功\n\n```json\n{\n\t\"sql@\": {\n\t\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_user_role\",\n        \"Sys_user_role\": {\n          \"@column\": \"user_id\",\n\t\t  \"role_id{}\": [\"0bb92d96-8ca6-469e-91e8-60308ce5b835\"]\n        }\n    },\n    \"Sys_user:aa\": {\n    \t\"@method\": \"DELETE\",\n        \"id{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n![image](https://user-images.githubusercontent.com/12228225/204080050-e6f04fe6-319e-45b7-b1b2-bf4cda4ab2db.png)\n\nPUT 子查询 修改\n\n```json\n{\n   \"sql@\": {\n\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role_permission\",\n        \"Sys_role_permission\": {\n             \"@column\": \"role_id\",\n\t     \"id{}\": [\"ba2634f8-0bdc-4b50-9c5e-47786b1536ef\"]\n        }\n    },\n    \"Sys_role\": {\n    \t\"@method\": \"PUT\",\n        \"id{}@\": \"sql\",\n        \"role_code\": \"code-subrange-5\",\n        \"role_name\": \"角色-subrange-5\"\n    },\n    \"@explain\": true\n}\n```\n\n![image](https://user-images.githubusercontent.com/12228225/204080072-8f605595-cd8c-474b-975f-4ac97fb92a26.png)\n\n#### bug修复\n\n删除操作 主表 和 子查询 是同一张表\nmysql8以下 非with-as表达式 会报错:\n\"msg\": \"You can't specify target table 'User' for update in FROM clause\",\n\n需要调整sql语句,将子查询包一层(select * from (子查询) as xxx)\nDELETE FROM `housekeeping`.`User`\nWHERE ( (`username` IN (SELECT * FROM (SELECT `username` FROM `housekeeping`.`User` WHERE ( (`username` = 'test1') )) as a) ) )\n\n![image](https://user-images.githubusercontent.com/12228225/204080126-e1f7c82a-2f09-409d-b3f2-fe25badea180.png)\n\n![image](https://user-images.githubusercontent.com/12228225/204080131-0c15404d-3045-4d01-bd89-d2a1f1fa0360.png)\n\n\n### must、refuses判断、delete、PUT支持 ref\n\n```json\n{\n    \"sql@\": {\n\t\"@method\": \"GET\",\n        \"with\": true,\n        \"from\": \"Sys_role_permission\",\n        \"Sys_role_permission\": {\n            \"@column\": \"id\",\n\t    \"role_id{}\": [\"94f79f0b-331b-4cc5-bfc0-ebfc47d00f13\"]\n        }\n    },\n    \"Sys_role_permission\": {\n    \t\"@method\": \"DELETE\",\n        \"id{}@\": \"sql\"\n    },\n    \"explan\": true\n}\n```\n\n![image](https://user-images.githubusercontent.com/12228225/204080150-28972226-37e0-4280-962a-83f7ac12d37c.png)\n"
  },
  {
    "path": "README.md",
    "content": "Tencent is pleased to support the open source community by making APIJSON available.   <br/>\nCopyright (C) 2020 Tencent.  All rights reserved. <br/>\nThis source code is licensed under the Apache License Version 2.0 <br/>\n\n<h1 align=\"center\" style=\"text-align:center;\">\n  APIJSON\n</h1>\n\n\n<p align=\"center\">🏆 Real-Time no-code, powerful and secure ORM 🚀 <br />providing APIs and Docs without coding by Backend, and Frontend can customize response JSONs</p>\n\n<p align=\"center\" >\n  <a href=\"https://github.com/Tencent/APIJSON/blob/master/README-Chinese.md\">&nbsp;中文版&nbsp;</a>\n  <a href=\"https://github.com/Tencent/APIJSON/blob/master/Document.md\">&nbsp;Document&nbsp;</a>\n  <a href=\"https://search.bilibili.com/all?keyword=APIJSON\">&nbsp;Video&nbsp;</a>\n  <a href=\"http://apijson.cn/api\">&nbsp;Test&nbsp;</a>\n  <a href=\"https://deepwiki.com/Tencent/APIJSON\">Ask AI</a>\n</p>\n\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/MySQL-5.7%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/PostgreSQL-9.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/SQLServer\"><img src=\"https://img.shields.io/badge/SQLServer-2012%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Oracle\"><img src=\"https://img.shields.io/badge/Oracle-12C%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/DB2\"><img src=\"https://img.shields.io/badge/DB2-7.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/MariaDB-10.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TiDB-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/CockroachDB-25.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/openGauss-5.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Dameng\"><img src=\"https://img.shields.io/badge/Dameng-7.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/Kingbase-8.6%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Milvus-2.2.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/DuckDB\"><img src=\"https://img.shields.io/badge/DuckDB-1.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONBoot-MultiDataSource\"><img src=\"https://img.shields.io/badge/SurrealDB-2.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/ClickHouse\"><img src=\"https://img.shields.io/badge/ClickHouse-21.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/PostGIS-3.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Elasticsearch\"><img src=\"https://img.shields.io/badge/Elasticsearch-7.17%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Manticore\"><img src=\"https://img.shields.io/badge/ManticoreSearch-7.4%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Presto\"><img src=\"https://img.shields.io/badge/Presto-0.277%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/Trino\"><img src=\"https://img.shields.io/badge/Trino-400%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TDSQL-cloud-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL\"><img src=\"https://img.shields.io/badge/TencentDB-cloud-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Redis\"><img src=\"https://img.shields.io/badge/Redis-5.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-MultiDataSource-Kafka\"><img src=\"https://img.shields.io/badge/Kafka-3.2%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Snowflake-7.0%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Databricks-13.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/chenyanlann/APIJSONBoot_Hive\"><img src=\"https://img.shields.io/badge/Hive-3.1%2B-brightgreen.svg?style=flat\"></a>  \n  <a href=\"https://github.com/chenyanlann/APIJSONBoot_Hive\"><img src=\"https://img.shields.io/badge/Hadoop-3.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/MongoDB-Altlas%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/Cassandra-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/InfluxDB-2.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/TDengine\"><img src=\"https://img.shields.io/badge/TDengine-2.6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/PostgreSQL\"><img src=\"https://img.shields.io/badge/TimescaleDB-17.1%2B-brightgreen.svg?style=flat\"></a>\n  <a ><img src=\"https://img.shields.io/badge/IoTDB-1.3%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONBoot-MultiDataSource\" ><img src=\"https://img.shields.io/badge/DataBend-1.2%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server\"><img src=\"https://img.shields.io/badge/Java-1.8%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/glennliao/apijson-go\"><img src=\"https://img.shields.io/badge/Go-1.18%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/j2go/apijson-go\"><img src=\"https://img.shields.io/badge/Go-1.16%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/liaozb/APIJSON.NET\"><img src=\"https://img.shields.io/badge/CSharp-2.1%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/kvnZero/hyperf-APIJSON\"><img src=\"https://img.shields.io/badge/PHP-8.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/kevinaskin/apijson-node\"><img src=\"https://img.shields.io/badge/Node.js-ES6%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/zhangchunlin/uliweb-apijson\"><img src=\"https://img.shields.io/badge/Python-3%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://gitee.com/APIJSON/panda-base\"><img src=\"https://img.shields.io/badge/Rust-1.90%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-Script\"><img src=\"https://img.shields.io/badge/Lua-5.2%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONDemo/pom.xml#L48-L52\"><img src=\"https://img.shields.io/badge/Spring-4.3.2%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONDemo/pom.xml#L48-L52\"><img src=\"https://img.shields.io/badge/SpringBoot-1.4.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONFinal/pom.xml#L59-L68\"><img src=\"https://img.shields.io/badge/JFinal-3.5%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/vincent109/apijson-nutz\"><img src=\"https://img.shields.io/badge/Nutz-2.4.2%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server/APIJSONDemo-ShardingSphere\"><img src=\"https://img.shields.io/badge/ShardingSphere-5.4.1%2B-brightgreen.svg?style=flat\"></a>\n</p>\n<p align=\"center\" >\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Android\"><img src=\"https://img.shields.io/badge/Android-4.0%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-iOS\"><img src=\"https://img.shields.io/badge/iOS-7%2B-brightgreen.svg?style=flat\"></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-JavaScript\"><img src=\"https://img.shields.io/badge/JavaScript-ES6%2B-brightgreen.svg?style=flat\"></a>\n</p>\n\n<p align=\"center\" >\n  <img src=\"https://oscimg.oschina.net/oscnet/up-3299d6e53eb0534703a20e96807727fac63.png\" />\n</p>\n\n---\n\n* ### [1.About](#1)\n* ### [2.Backend usage](#2)\n* ### [3.Frontend usage](#3)\n* ### [4.Contributing](#4)\n* ### [5.Releases](#5)\n* ### [6.Creator](#6)\n* ### [7.Donating](#7)\n\n<br />\n\n## <h2 id= \"1\">1. About <h2/>\n\nAPIJSON is a JSON based internet communication protocol and an ORM library  <br />\nthat largely simplifies the process of back-end API development.  <br />\nIt also allows users to get data more quickly with self-defined form and fewer endpoints requests.\n\n### Features:\n#### For getting data:\nYou can get any data by defining the specific information you want and send it to the server. <br />\nYou can get different types of data by making just one request to the server. <br />\nIt's very convenient and flexible, and dosen't require different API endpoints with multiple requests. <br />\nIt provides CRUD(read and write), Fuzzy Search, Remote Function Calls, etc. <br />\nYou can also save duplicate data, see request history, etc. <br />\n\n#### For API design:\nAPIJSON largely reduces API developers' workload by reducing most api design and documentation work. <br />\nWith APIJSON, client developers will no longer be suffered from possible errors in documents, <br />\nand it saves communication between server developers and client developers about APIs or documentations. <br />\nServer developers no longer need to worry about compatibility of APIs and documents with legacy apps. <br />\n\n![0F85206E116CCEE74DB68E5B9A3AEDAE](https://user-images.githubusercontent.com/5738175/196148099-d3a9e0ba-93e5-4e1a-a4f8-a714083c6f7e.jpg)\n#### Song Firework-Katy Parry(Modified for APIJSON)\nDo you ever feel like a backend slave <br />\nRepeating CRUD, wanting to make a change? <br />\nDo you ever feel, APIs' so paper thin <br />\nLike a house of cards, one blow from cavin' in? <br />\nDo you ever feel they always complain? <br />\nUrging doc and feedback bugs, even ask your refactoring <br />\nDo you know that there's still a chance for you? <br />\n'Cause there's a powerful tool <br />\nYou just gotta depend and configure <br />\nAnd let it init <br />\nJust start APIs <br />\nThey are so easy to try <br />\n'Cause baby, you're a firework <br />\nCome on, show 'em what you're worth <br />\nMake 'em go, \"Oh, oh, oh\" <br />\nAs you give 'em an A-T-M <br />\nBaby, you're a firework <br />\nCome on, let them serve themselves <br />\nMake 'em go, \"Oh, oh, oh\" <br />\nYou're gonna leave 'em all in awe, awe, awe. <br />\n\n<br/>\n\n**Tired with endless arguments about HTTP API dev or use?** <br/>\n**Use APIJSON-the ORM for providing infinity codeless CRUD APIs that fit almost all your needs.** <br />\n**Unfold the Power(In Your Soul) with ⭐Star & Clone.**\n\n### APIJSON Show\n#### Postman test APIJSON\n![](https://static.oschina.net/uploads/img/201711/12230359_f7fQ.jpg)\n<br/>\n\n#### APIAuto test APIJSON\nNote: The UI is APIAuto, the URL+JSON is APIJSON<br/>\n<br />\n<p align=\"center\" >\n  <a >APIJSON: query multi related tables, flexible data structures. APIAuto: multi test accounts, easily share test cases</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-bbbec4fc5edc472be127c02a4f3cd8f4ec2.JPEG) \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_associate.gif) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto: generate request code for frontend/clients or Python test cases, easily download code</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-637193bbd89b41c3264827786319e842aee.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto: auto save test records, auto generate API doc, requests shortcut, easily replay</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-7dcb4ae71bd3892a909e4ffa37ba7c1d92a.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >APIAuto: auto regression test without code, annotation, comment, etc.)</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-c1ba774f8e7fcc5adcdb05cad5bd414d766.JPEG) \n\n<br />\n<p align=\"center\" >\n  <a >A picture is worth a thousand words - some basic features show for APIJSON</a>\n</p> \n\n![](https://oscimg.oschina.net/oscnet/up-e21240ef3770326ee6015e052226d0da184.JPEG) \n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_query_summary.gif) \n\n\n## <h2 id=\"2\">2.Backend usage<h2/>\nYou can skip this step and use 'apijson.cn:8080'. <br />\nSee https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/README-English.md\n\n<br />\n\n## <h2 id=\"3\">3. Frontend usage<h2/> \nYou can skip this step and use [APIAuto](https://github.com/TommyLemon/APIAuto) or download App.<br />\nSee [Android](https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Android/README-English.md), [iOS](https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-iOS/README-English.md) or [JavaScript](https://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-JavaScript/README-English.md)<br />\n\n### Download App\n\nSimple demo App for testing APIJSON<br />\n[APIJSONTest.apk](http://files.cnblogs.com/files/tommylemon/APIJSONTest.apk)\n\t\nComplex production App like Twitter tweets<br />\n[APIJSONApp.apk](http://files.cnblogs.com/files/tommylemon/APIJSONApp.apk)\n\n<br />\n\t\n## <h2 id=\"4\">4. Contributing<h2/> \n\t\nWe are always looking for more developers to help implementing new features, fix bugs, etc. <br />\nPlease have a look at the [open issues](https://github.com/Tencent/APIJSON/issues) before opening a new one. <br />\n\nFork the project and send a pull request.<br />\n\nPlease also ⭐Star the project!\n<br />\n\n## <h2 id=\"5\">5. Releases<h2/> \n\t\nSee the latest release [here](https://github.com/Tencent/APIJSON/releases)\n\n<br />\n\n## <h2 id=\"6\">6. Creator<h2/> \t\n\t\nhttps://github.com/TommyLemon <br />\n![](https://github.com/user-attachments/assets/cef2bd45-b20d-469e-8781-1d647cf0477f)\n\nIf you have any questions or suggestions, you can [create an issue](https://github.com/Tencent/APIJSON/issues) or [send me an e-mail](mailto:tommylemon@qq.com).\n\n<br />\n<br />\n\n### Users of APIJSON:\n\nhttps://github.com/Tencent/APIJSON/issues/187 \n<div style=\"float:left\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/126525534-461c3e33-57b1-4630-af7f-f1238ca4ab98.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/126525251-c05e64c6-6b60-4457-a46e-dea7dcfb80cd.png\" height=\"75\">\n  <br />\n  <img src=\"https://user-images.githubusercontent.com/5738175/195065513-c581e958-2386-4a34-8b78-f48e87d1e1f2.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195063764-dcc272a0-3c2c-4073-8f22-c501c22a0844.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195063874-9d37425d-f220-445f-8554-655d5c02931b.png\" height=\"75\">\n  <br />\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.transsion.com.jpeg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/shebaochina.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.xmfish.com.gif\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.xxwolo.com.jpeg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/t-think.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/xm.juhu.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.aipaipai-inc.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.8sso.com.jpeg\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758356-fbc89569-8b34-49d4-9f8e-272a8406440d.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.shulian8.com.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758846-1c055ae1-c235-498b-a64c-902a6068af76.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.hngtrust.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.hec-bang.com.png\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.toutou.com.cn.jpg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.yto.net.cn.jpg\" height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.lepinyongche.com.jpg\"  height=\"75\">\n  <img src=\"https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/User/www.aupup.com.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758697-3267f031-a7bc-44f2-84bb-06a4a7e30a75.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758188-40294d75-ef7d-4ddc-9af8-5b8c195839cf.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758198-8ec01213-18f7-43d5-9942-7c49a898ccef.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/95326431/194802562-e7f92b39-edbb-401f-806a-1a22513e785e.png\"  height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758742-28d79efd-6645-44ee-bb50-844aa39b25fe.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758753-0a3bb998-a533-4388-8224-4f9d743ff576.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758795-e49e3eae-12ba-4399-a8e1-75db94cb0a99.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758984-0fe2fcd9-5119-46d3-9e22-4632556c0b9e.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195758995-db762406-627b-4ea5-8397-b99bb5711cce.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759031-bdcf4146-34cb-470c-a576-37d4e8fdca24.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759040-c7db99ff-3404-411d-b9ba-23547aaf1509.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759093-927fd5c3-9e1e-4648-8a35-c9d97630d086.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/10636d69-1d54-4666-aa8a-472c4ecb9413\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759079-ffc4483e-46a6-4e28-a0e0-25186ea008ab.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759186-a90a04db-0bd4-47bc-bab0-c160dcf48e53.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759204-7bdb09f5-2194-41c1-8e59-1461bd5ff4c1.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759227-2e5d42ae-b42d-4702-801d-566e70809e79.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759318-b0edad0d-9f6c-44b9-97a4-6c566880bc4b.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759239-1cb44526-abfa-4800-8d65-233d04b7c0d3.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759268-b4ad2945-704e-495c-b2b0-d0166dc5e33a.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/195759292-baa3924c-cf56-49cf-820c-d1e0a88cac3b.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652404-927a945e-22f5-42f8-99da-3a0863a5a3b5.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226653817-fcc57051-53e2-4c8d-bda6-3effba4032ee.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/7c71b8f9-f1cc-4305-8e97-c212f476e377\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652405-561963cb-73e4-4d65-986c-ebfafcfe7b73.jpeg\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226652403-92546c06-6dc4-4f46-b697-02a4073833f8.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226657098-d63c0dd1-24d0-4819-9045-b8213ab2e31f.png\" height=\"75\">\n  <img src=\"https://user-images.githubusercontent.com/5738175/226657183-b6bbf284-3eb4-431e-8549-6356b7929e45.png\" height=\"75\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/9de7c199-4f29-44ed-9cb6-ad5e4fa44dfa\" height=\"90\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/d7155a65-22f7-49c6-8354-c309f36e4065\" height=\"60\">\n  <img src=\"https://github.com/Tencent/APIJSON/assets/5738175/f5a6ec8d-d9a8-49d0-a284-c50f1376647e\" height=\"75\">\n\n<br />\n\t\n[More APIJSON Users](https://github.com/Tencent/APIJSON/issues/73)\n\n### Contributers of APIJSON:\nContributers for the APIJSON core project(6 Tencent engineers, 1 Microsoft engineer, 1 Zhihu architect, 1 Bytedance(TikTok) engineer, 1 NetEase engineer, 1 Zoom engineer, 1 YTO Express engineer, 1 Zhilian engineer, 1 UC student、3 SUSTech students, etc.): <br />\nhttps://github.com/Tencent/APIJSON/blob/master/CONTRIBUTING.md <br />\n<div style=\"float:left\">\n  <a href=\"https://github.com/TommyLemon\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\" \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/41\"><img src=\"https://avatars0.githubusercontent.com/u/39320217?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/119\"><img src=\"https://avatars1.githubusercontent.com/u/25604004?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/223\"><img src=\"https://avatars.githubusercontent.com/u/49295281?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/43\"><img src=\"https://avatars0.githubusercontent.com/u/23173448?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/47\"><img src=\"https://avatars2.githubusercontent.com/u/31512287?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/212\"><img src=\"https://avatars.githubusercontent.com/u/8936328?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/70\"><img src=\"https://avatars1.githubusercontent.com/u/22228201?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/74\"><img src=\"https://avatars0.githubusercontent.com/u/1274536?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/92\"><img src=\"https://avatars3.githubusercontent.com/u/6327228?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/103\"><img src=\"https://avatars0.githubusercontent.com/u/25990237?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/194\"><img src=\"https://avatars0.githubusercontent.com/u/3982329?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/219\"><img src=\"https://avatars.githubusercontent.com/u/7135770?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/222\"><img src=\"https://avatars.githubusercontent.com/u/49233056?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/221\"><img src=\"https://avatars.githubusercontent.com/u/17545585?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/217\"><img src=\"https://avatars.githubusercontent.com/u/30771966?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/190\"><img src=\"https://avatars3.githubusercontent.com/u/25056767?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/69\"><img src=\"https://avatars0.githubusercontent.com/u/13880474?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/72\"><img src=\"https://avatars1.githubusercontent.com/u/10663804?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/33\"><img src=\"https://avatars1.githubusercontent.com/u/5328313?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/235\"><img src=\"https://avatars.githubusercontent.com/u/17243165?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/233\"><img src=\"https://avatars.githubusercontent.com/u/1252459?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/250\"><img src=\"https://avatars.githubusercontent.com/u/44310040?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/253\"><img src=\"https://avatars.githubusercontent.com/u/19265050?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/278\"><img src=\"https://avatars.githubusercontent.com/u/4099373?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/279\"><img src=\"https://avatars.githubusercontent.com/u/28685375?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/280\"><img src=\"https://avatars.githubusercontent.com/u/60541766?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/283\"><img src=\"https://avatars.githubusercontent.com/u/50007106?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/284\"><img src=\"https://avatars.githubusercontent.com/u/45117061?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/285\"><img src=\"https://avatars.githubusercontent.com/u/32100214?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/287\"><img src=\"https://avatars.githubusercontent.com/u/62465397?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/288\"><img src=\"https://avatars.githubusercontent.com/u/55579125?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/291\"><img src=\"https://avatars.githubusercontent.com/u/17522475?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/293\"><img src=\"https://avatars.githubusercontent.com/u/53826144?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/295\"><img src=\"https://avatars.githubusercontent.com/u/11210385?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/311\"><img src=\"https://avatars.githubusercontent.com/u/22066942?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/325\"><img src=\"https://avatars.githubusercontent.com/u/33931153?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Tencent/APIJSON/pull/443\"><img src=\"https://avatars.githubusercontent.com/u/95326431?s=40&v=4\"  height=\"54\" width=\"54\" ></a>\n</div>\n<br />\n\t\nAuthors of other projects for ecosystem of APIJSON(2 Tencent engineers, 1 BAT(Baidu/Alibaba/Tencent) expert, 1 Microsoft engineer, 2 Bytedance(TikTok) engineers,  1 Digital China engineer & Apache dubbo2js author, etc.): <br />\nhttps://github.com/search?o=desc&q=apijson&s=stars&type=Repositories <br />\nhttps://search.gitee.com/?skin=rec&type=repository&q=apijson&sort=stars_count <br />\n<div style=\"float:left\">\n  <a href=\"https://github.com/APIJSON/apijson-orm\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/liaozb/APIJSON.NET\"><img src=\"https://avatars3.githubusercontent.com/u/12622501?s=400&v=4\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/tiangao/apijson-go\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/43/130007_tiangao_1578918889.png!avatar200\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/qq547057827/apijson-php\"><img src=\"https://avatars3.githubusercontent.com/u/1657532?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/xianglong111/APIJSON-php\"><img src=\"https://avatars.githubusercontent.com/u/9738743?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/kevinaskin/apijson-node\"><img src=\"https://avatars3.githubusercontent.com/u/20034891?s=400&v=4\"\n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TEsTsLA/apijson\"><img src=\"https://avatars2.githubusercontent.com/u/17310639?s=400&v=4\"\n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/zhangchunlin/uliweb-apijson\"><img src=\"https://avatars0.githubusercontent.com/u/359281?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/crazytaxi824/APIJSON\"><img src=\"https://avatars3.githubusercontent.com/u/16500384?s=400&v=4\" \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/luckyxiaomo/APIJSONKOTLIN\"><img src=\"https://avatars2.githubusercontent.com/u/42728605?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Zerounary/APIJSONParser\"><img src=\"https://avatars2.githubusercontent.com/u/31512287?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson-framework\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/APIJSON-Demo\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson-column\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/jerrylususu/apijson_todo_demo\"><img src=\"https://avatars.githubusercontent.com/u/17522475?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/vcoolwind/apijson-practice\"><img src=\"https://avatars.githubusercontent.com/u/22070287?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/rainboy-learn/apijson-learn\"><img src=\"https://avatars.githubusercontent.com/u/43025876?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/greyzeng/apijson-sample\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/367/1102309_greyzeng_1578940307.png!avatar200\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/zhiyuexin/ApiJsonByJFinal\"><img src=\"https://avatar.gitee.com/uploads/90/490_zhiyuexin.jpg!avatar100?1368664499\"  \n height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/Airforce-1/SpringServer1.2-APIJSON\"><img src=\"https://avatars3.githubusercontent.com/u/6212428?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://gitee.com/JinShuProject/JinShuApiJson\"><img src=\"https://portrait.gitee.com/uploads/avatars/user/232/698672_maxiaoji_1578931055.jpg!avatar200\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/qiujunlin/APIJSONDemo\"><img src=\"https://avatars.githubusercontent.com/u/50007106?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/chenyanlann/APIJSONDemo_ClickHouse\"><img src=\"https://avatars.githubusercontent.com/u/62465397?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/andream7/apijson-db2\"><img src=\"https://avatars.githubusercontent.com/u/60541766?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/APIJSON-Android-RxJava\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/APIAuto\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/UnitAuto\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/vincentCheng/apijson-doc\"><img src=\"https://avatars3.githubusercontent.com/u/6327228?s=400&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/ruoranw/APIJSONdocs\"><img src=\"https://avatars.githubusercontent.com/u/25990237?s=460&u=2143b95e5ed39185f2a03d66fbb5638795e16d5a&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/HANXU2018/APIJSON-DOC\"><img src=\"https://avatars.githubusercontent.com/u/45117061?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/kenlig/apijsondocs\"><img src=\"https://avatars.githubusercontent.com/u/28685375?v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/apijson.org\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/pengxianggui/apijson-builder\"><img src=\"https://avatars2.githubusercontent.com/u/16299169?s=460&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/APIJSON/AbsGrade\"><img src=\"https://avatars.githubusercontent.com/u/41146037?s=200&v=4\"  height=\"54\" width=\"54\" ></a>\n  <a href=\"https://github.com/TommyLemon/Android-ZBLibrary\"><img src=\"https://avatars1.githubusercontent.com/u/5738175?s=400&u=5b2f372f0c03fae8f249d2d754e38971c2e17b92&v=4\"  height=\"54\" width=\"54\" ></a>\n</div>\n<br />\n\nThanks to all contributers of APIJSON!\n\n<br />\n\n### Statistics\nHundreds of employees from big famous companies(Tencent, Google, Apple, Microsoft, Amazon, Huawei, Alibaba, Paypal, Meituan, Bytedance(TikTok), IBM, Baidu, JD, NetEase, Kuaishou, Shopee, etc.) starred, <br >\na lot of employees from big famous companies(Tencent, Huawei, Microsoft, Zoom, etc.) created PR/Issue, thank you all~ <br >\n[![Stargazers over time](https://starchart.cc/Tencent/APIJSON.svg)](https://starchart.cc/Tencent/APIJSON)\n<img width=\"948\" alt=\"image\" src=\"https://github.com/Tencent/APIJSON/assets/5738175/2784e399-11c8-4eeb-8257-44533df61827\">\n<img width=\"948\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/195752839-554d0204-aa5d-48d8-b838-d1a0cb0e8690.png\">\n<img width=\"948\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/195752907-a09d9505-beb3-47a6-b7b9-079b58964b4d.png\">\n\n\n<br />\n"
  },
  {
    "path": "Roadmap.md",
    "content": "\n<h1 align=\"center\" style=\"text-align:center;\">\n  APIJSON 规划及路线图\n</h1>\n\n\n### 新增功能\n部分功能描述可在 [APIAuto](https://github.com/TommyLemon/APIAuto) 上查看 <br />\n账号 13000002020 密码 123456 <br />\nhttp://apijson.cn/api <br />\n\n##### 基本原则\n1.一定要有相关的应用场景，不能是伪需求，最好举例说明 <br />\n2.能用 远程函数 或者 @raw 等现有功能很好地实现的，都不新增，避免冗余 <br />\n3.不影响现有功能的使用，不能让现有功能不可用或者使用更复杂 <br />\n\n#### 新增支持假删除\n20230106 更新：已支持，感谢 @cloudAndMonkey 的贡献 <br />\nhttps://github.com/Tencent/APIJSON/pull/493\n\n一般对于互联网项目，数据非常重要，基本不会物理删除， <br />\n只是用 is_deleted 等字段来标记已删除，然后 CRUD 时默认过滤已标记的记录。 <br />\n这个功能非常必要，可以通过重写 SQLConfig.isFakeDelete() 来标记， <br />\n然后如果 true，则把 DELETE 改为 PUT 并且通过重写 <br />\nSQLConfig.onFakeDelete(Map<String, Object> map) 来新增条件： <br />\nGET: map.put(\"deleted\", 0) <br />\nPUT:  map.put(\"deleted\", 0) <br />\nDELETE:  map.put(\"deleted\", 1) <br />\nPOST:  用不上，不处理 <br />\n当然也可以再加一个删除时间 deletedTime 之类的。 <br />\n <br />\n还可以在 apijson-framework 层支持 Access 表新增字段 deletedKey, deletedValue, notDeletedValue， <br />\n然后读表自动配置是否为假删除 StringUtil.isNotEmpty(deletedKey, true) ，是假删除时 put 相应键值对。 <br />\n \n#### 新增支持 @having& \n20220328 更新：5.0.0 已支持 <br />\nhttps://github.com/Tencent/APIJSON/releases/tag/5.0.0\n\n来实现内部条件 AND 连接，原来的 @having 由 AND 连接变为 OR 连接，保持 横或纵与 的统一规则。<br />\n@having! 必须性不大，可通过反转内部条件来实现，但如果实现简单、且不影响原来的功能，则可以顺便加上。<br />\n\n#### 新增支持 @column!\n20210415 更新：已提供字段插件 [apijson-column](https://github.com/APIJSON/apijson-column)，支持 字段名映射 和 !key 反选字段。\n\n这个只在 [apijson-framework](https://github.com/APIJSON/apijson-framework) 支持，需要配置每个接口版本、每张表所拥有的全部字段，然后排除掉 @column! 的。<br />\n可新增一个 VersionedColumn 表记录来代替 HashMap 代码配置。<br />\n需要注意的是，可能前端传参里既有 @column 又有 @column! ，碰到这种情况：<br />\n如果没有重合字段就忽略 @column! ，只让 @column 生效；<br />\n如果有有重合字段，则抛异常，转为错误码和错误信息返回。<br />\n\n#### 新增支持 TSQL 的 @explain\n20220809 更新：已支持 Oracle 的 EXPLAIN，感谢 @ifooling 的贡献 <br />\nhttps://github.com/Tencent/APIJSON/pull/434\n\n目前 APIJSON 支持 [Oracle](https://github.com/APIJSON/APIJSON-Demo/tree/master/Oracle), [SQL Server](https://github.com/APIJSON/APIJSON-Demo/tree/master/SQLServer), DB2 这 3 种 TSQL 数据库。<br />\n但是 \"@explain\": true 使用的是 SET STATISTICS PROFILE ON(具体见 [AbstractSQLConfig](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java) 和 [AbstrctSQLExecutor](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstrctSQLExecutor.java))  <br />\n执行后居然是 SELECT 查到的放在默认的 ResultSet，性能分析放在 moreResult，<br />\n因为这个问题目前以上两个数据库的性能分析 @explain 实际并不可用，需要改用其它方式或解决现有方式的 bug。<br />\n\n#### 新增支持 page 从 1 开始\n目前只能从 0 开始，实际使用 1 更广泛，而且这方面用户习惯很强，支持它成本也不高。 <br />\n[Parser](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/Parser.java) 新增 DEFAULT_QUERY_PAGE 和 getDefaultQueryPage， <br />\n与 DEFAULT_QUERY_COUNT 和 getDefaultQueryCount 统一， <br />\n方便前端直接用页码的值传参，以及 info.page 的值来渲染页码。 <br />\n建议在 [AbstractParser](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java)，[AbstractSQLConfig](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java) 用到 page 的地方判断 getDefaultQueryPage，做兼容处理。 <br />\n\n#### 新增支持分布式\n\n \"@url\": \"http://apijson.cn:8080/get\"\n```js\n{\n    \"User\": {\n        \"@url\": \"http://apijson.cn:8080/get\"  //转发给其它服务器执行\n    },\n    \"[]\": {\n        \"Comment\": {\n            \"userId@\": \"User/id\"\n        }\n    },\n    \"@explain\": true\n}\n```\n考虑到引用赋值的依赖关系要保持先后顺序，必须同步阻塞处理。<br />\n或许可以加一个 ~ 前缀表示异步？例如 \"@url\": \"\\~http://apijson.cn:8080/get\"，由调用方保证没有被下方对象依赖。<br />\n\n\n#### 新增支持 Union\n\n虽然可以通过 INNER JOIN + 条件 OR 连接来替代它的功能，但没法达到它能用索引的性能。<br />\n支持 UNION 很有必要，但 UNION ALL 几乎没有需求，如果实现简单、且不影响原来的功能，则可以顺便加上。 <br />\n\n\"@union\": Integer  // 0 - 不使用，1 - UNION，2 - UNION ALL\n```js\n{\n    \"[]\": {\n        \"User\": {\n            \"name~\": \"a\",\n            \"tag~\": \"a\",\n            \"@combine\": \"name~,tag~\",    \n            \"@union\": 1  //将 @combine 中的 N 个 OR 连接字段用 UNION 替换，原本一条 SQL 需要拆分成 N 条 SQL 来 UNION \n        }\n    },\n    \"@explain\": true\n}\n```\n生成的 SQL\n```sql\nSELECT * FROM `sys`.`apijson_user` WHERE ( (`name` REGEXP BINARY 'a') OR (`tag` REGEXP BINARY 'a') ) LIMIT 10 OFFSET 0\n```\n需要变为\n```sql\nSELECT * FROM `sys`.`apijson_user` WHERE ( (`name` REGEXP BINARY 'a') )\nUNION\nSELECT * FROM `sys`.`apijson_user` WHERE ( (`tag` REGEXP BINARY 'a') )\nLIMIT 10 OFFSET 0\n```\n\n\n#### 新增支持 With\n20221126 更新：已支持，感谢 @cloudAndMonkey 的贡献 <br /> \nhttps://github.com/Tencent/APIJSON/pull/481\n\n可以减少子查询执行次数，提高性能。\n```js\n{   //看看关注的人最近有什么动态（分享、评论）\n    \"sql@\": {\n        \"with\": true,  //生成 WITH(SELECT id ...) AS `sql`\n        \"from\": \"User\",\n        \"User\": {\n            \"@column\": \"id\",\n            \"@role\": \"CONTACT\"\n        }\n    },\n    \"Moment[]\": {\n        \"Moment\": {\n            \"userId{}@\": \"sql\",\n            \"@order\": \"date-\"\n        }\n    },\n    \"Comment[]\": {\n        \"Comment\": {\n            \"userId{}@\": \"sql\",\n            \"@order\": \"date-\"\n        }\n    },\n    \"@explain\": true\n}\n```\n生成的 SQL\n```sql\nSELECT * FROM `sys`.`Moment` WHERE ( (`userId` IN (SELECT `id` FROM `sys`.`User` WHERE `id` IN($contactIdList)) ) ) ORDER BY `date` DESC LIMIT 10 OFFSET 0\n```\n和\n```sql\nSELECT * FROM `sys`.`Moment` WHERE ( (`userId` IN (SELECT `id` FROM `sys`.`User` WHERE `id` IN($contactIdList)) ) ) ORDER BY `date` DESC LIMIT 10 OFFSET 0\n```\n\n需要变为\n```sql\nWITH (SELECT `id` FROM `sys`.`User` WHERE `id` IN($contactIdList)) AS `sql`\nSELECT * FROM `sys`.`Moment` WHERE ( (`userId` IN `sql` ) ) ORDER BY `date` DESC LIMIT 10 OFFSET 0\n```\n和\n```sql\nWITH (SELECT `id` FROM `sys`.`User` WHERE `id` IN($contactIdList)) AS `sql`\nSELECT * FROM `sys`.`Comment` WHERE ( (`userId` IN `sql` ) ) ORDER BY `date` DESC LIMIT 10 OFFSET 0\n```\n\n\n#### 新增支持 Case\n【更新：不用实现了，直接按 SQL 的语法写 CASE WHEN，然后用 @raw】\n实现远程函数也不方便的 SQL 内字段转换 CASE WHEN THEN ，<br />\n暂时还没有想好如何设计。如果是 SQL 原来的写法，则有点繁琐。<br />\n\n\n#### 新增支持 id 与其它字段同时作为增删改条件\nAbstractVerifier.IS_UPDATE_MUST_HAVE_ID_CONDITION = false <br />\n就同时支持 id、其它条件删除 <br />\nhttps://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java#L84-L86\n\n但因为 Operation 没有 AT_LEAST_ONE/ANY_ONE 这样的操作， <br />\n所以如果只配置一条规则，只能允许 MUST 配置传一种条件，不能单独传 id/其它字段。 <br />\n <br />\n如果都传了，因为 id 强制作为 AND 条件，所以不能和其它条件 OR， <br />\n可以配置两条不同规则，用不同的 tag 对应使用不同的条件。 <br />\n <br />\nmethod: DELETE <br />\n\n通过 id 删除 \n```\ntag: Comment-by-id // 当然写成 Comment:id 等其它任何不符合表名格式的名称都可\nstructure: ... \"MUST\":\"id\" ...\n```\n <br />\n \n通过 date 条件删除 \n```\ntag: Comment-by-date\nstructure: ... \"MUST\":\"date\" ...\n```\n\n <br />\n如果想只配置一条规则，则 Operation 加上 AT_LEAST_ONE/ANY_ONE ，然后配置 \n\n```\ntag: Comment\nstructure: ... \"AT_LEAST_ONE\":\"id,date\" ... // 至少传其中一个\n```\n\n或 \n\n```\ntag: Comment\nstructure: ... \"ANY_ONE\":\"id,date\" ... // 必须传其中一个，不能同时传 2 个以上\n```\n <br />\n \nAT_LEAST_ONE/ANY_ONE 其中一个也可以通过扩展 MUST 来实现（目前看这种方式更好） <br />\n\"MUST\":\"id | date,其它\" 通过 | 或来表示其中任何一个，注意左右一定要各有一个空格，因为可能有 \"name|$\" \"id|{}\" 等包含 \"|\" 的 key <br />\nhttps://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/Operation.java\n\n <br />\n还可以设置更复杂的表达方式\n\n```\n\"MUST\":\"1:id | date,其它\" // id, date 必须传其中一个，且不能多传 <br />\n\"MUST\":\">=2:id | momentId|{} | date>=,其它\" // id, momentId|{}, date>= 必须至少其中 2 个\n\"MUST\":\"2+:id | momentId|{} | date>=,其它\" // id, momentId|{}, date>= 必须至少其中 2 个，替代 >= 2，更方便解析，并且不用考虑 >1, != 2 等其它各种不等式\n\"MUST\":\"2-:id | momentId|{} | date>=,其它\" // id, momentId|{}, date>= 最多传其中 2 个，替代 <= 2\n```\n\n <br />\n这样的话就不用加 Operation 了，不过 AbstractVerifier 仍然要处理下 REFUSE 和 MUST 的互斥关系 <br />\nhttps://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java#L1012-L1042\n<img width=\"1117\" alt=\"image\" src=\"https://user-images.githubusercontent.com/5738175/211016885-9e752b6c-94e5-46c0-b87d-a7be68387a9f.png\">\n\n##### 需求来源及具体讨论\nhttps://github.com/Tencent/APIJSON/pull/493#issuecomment-1373376359\n\n#### ...  //欢迎补充\n\n\n\n### 强化安全\nAPIJSON 提供了各种安全机制，可在目前的基础上新增或改进。\n\n20220504 更新：新增插件 apijson-router，可控地对公网暴露类 RESTful 简单接口，内部转成 APIJSON 格式请求来执行。 <br />\nhttps://github.com/APIJSON/apijson-router\n\n#### 防越权操作\n\n目前有 RBAC 自动化权限管理。<br />\nAPIJSONORM 提供 [@MethodAccess](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/MethodAccess.java) 注解来配置 <br />\n[apijson-framework](https://github.com/APIJSON/apijson-framework) 则使用 [Access 表](https://github.com/APIJSON/APIJSON-Demo/blob/master/MySQL/single/sys_Access.sql) 记录来配置 <br />\n在 [AbstractVerifier](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java) 中，假定真实、强制匹配。 <br />\n\n\n#### 防 SQL 注入\n\n目前有 预编译 + 白名单 校验机制。具体见 [AbstractSQLExecutor](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java)  和 [AbstractSQLConfig](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java) 。 <br />\n\n#### 防恶意请求\n\n目前有限流机制，getMaxQueryCount, getMaxUpdateCount, getMaxObjectCount, getMaxSQLCount, getMaxQueryDepth 等。 <br />\nhttps://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/Parser.java <br />\n\n\n#### ...  //欢迎补充\n\n\n### 提高性能\n20200205 更新：最近的及次大幅提升性能相关优化及 Release <br />\n[新增支持 ClickHouse、窗口函数 OVER、反引号 `key`、单引号 'value'；大幅提升单表数组查询性能](https://github.com/Tencent/APIJSON/releases/tag/4.8.0) <br />\n[4.6.0【性能】大幅提升数组内主表查询性能](https://github.com/Tencent/APIJSON/releases/tag/4.6.0) <br />\n[4.4.5【性能】大幅提升增删改的性能](https://github.com/Tencent/APIJSON/releases/tag/4.4.5) <br />\n\n#### 解析 JSON\n\n优化 [AbstractParser](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java) 和 [AbstractObjectParser](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractObjectParser.java) 解析 JSON 性能。 <br />\n\n#### 封装 JSON\n\n优化 [AbstractSQLExecutor](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java) 封装 JSON 性能。 <br />\n\n#### 拼接 SQL\n\n优化 [AbstractSQLConfig](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java) 拼接 SQL 性能。<br />\n[完善功能](https://github.com/Tencent/APIJSON/blob/master/Roadmap.md#%E5%AE%8C%E5%96%84%E5%8A%9F%E8%83%BD) 中 Union 和 With 也是优化 SQL 性能的方式。 <br />\n\n#### 读写缓存\n20230105 更新：新增 Redis 缓存 Demo\nhttps://github.com/APIJSON/APIJSON-Demo/commit/060a10e56818b31ab332770815467af9f052c261\n\n在 [AbstractParser](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java) 使用了 HashMap<String, JSONObject> queryResultMap 存已解析的对象、总数等数据。<br />\n在 [AbstractSQLExecutor](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java) 使用了 HashMap<String, JSONObject> cacheMap 存已通过 SQL 查出的结果集。<br />\n\n#### ...  //欢迎补充\n\n\n\n### 增强稳定\nAPIJSON 代码经过商业分析软件 [源伞Pinpoint](https://www.sourcebrella.com/) 的质检，报告说明 APIJSON 非常可靠。<br />\nhttps://github.com/Tencent/APIJSON/issues/48 <br />\n但我们需要再接再厉，尽可能做到 99.999% 可靠度，降低使用风险，让用户放心和安心。 <br />\n\n20200205 更新：已经解决了 [源伞科技](https://www.sourcebrella.com) 以上报告中的大部分问题 及 [奇安信代码卫士](https://github.com/QiAnXinCodeSafe) 发现的部分问题\nhttps://github.com/Tencent/APIJSON/issues/created_by/QiAnXinCodeSafe\n\n#### 减少 Bug\n\n##### [APIAuto](https://github.com/TommyLemon/APIAuto) 上统计的 bug\n账号 13000002000 密码 123456 <br />\nhttp://apijson.cn/api <br />\n\n##### 其它发现的 Bug\nhttps://github.com/Tencent/APIJSON/issues?q=is%3Aissue+is%3Aopen+label%3Abug <br />\n\n#### 完善测试\n\n##### 在 APIAuto-机器学习自动化接口管理平台 上传更多、更全面、更细致的测试用例、动态参数等\nhttp://apijson.cn/api <br />\n\n##### 在 UnitAuto-机器学习自动化单元测试平台 上传更多、更全面、更细致的测试用例、动态参数等\nhttp://apijson.cn/unit <br />\n\n\n### 完善文档\n20211112 更新：已在官网部署文档 http://apijson.cn/doc/zh\n20200205 更新：最近完善及更新了通用文档、上手文档、图文入门文档等，还对首页引导文档加了导航目录\nhttps://github.com/Tencent/APIJSON/blob/master/Navigation.md\n\n#### 中文文档\n\n##### 通用文档\nhttps://github.com/Tencent/APIJSON/blob/master/Document.md <br />\n\n##### 配置与部署\nhttps://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server <br />\n\n##### ...  //欢迎补充\n\n\n#### English Document\n\nTranslation for Chinese document. <br />\nhttps://github.com/Tencent/APIJSON/blob/master/README-English.md <br />\nhttps://github.com/Tencent/APIJSON/blob/master/Document-English.md <br />\nhttps://github.com/ruoranw/APIJSONdocs <br />\n\n\n\n### 丰富周边\n#### 新增或完善各种语言 ORM 库\n\nGo, Node(js/ts), C#, PHP, Kotlin, Ruby 等。<br />\nhttps://github.com/Tencent/APIJSON#%E7%94%9F%E6%80%81%E9%A1%B9%E7%9B%AE <br />\n\n#### 新增或完善 Demo\n\nJavaScript 前端，TypeScript 前端，微信等小程序，<br />\nAndroid 客户端，iOS 客户端，C# 游戏客户端等。<br />\nJava, C#, PHP, Node, Python 等后端 Demo 及数据。<br />\nhttps://github.com/APIJSON/APIJSON-Demo <br />\n\n#### 新增扩展\n目前官方有 apijson-column, apijson-router 两个插件\n\n##### 1.基于或整合 [APIJSONORM](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM) 或 [apijson-framework](https://github.com/APIJSON/apijson-framework) 来实现的库/框架\n\n##### 2.扩展 [APIJSONORM](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM) 或 [apijson-framework](https://github.com/APIJSON/apijson-framework) 功能的插件\n可以通过扩展对象关键词 @key，数组关键词 key，远程函数，重写部分方法等来实现。<br />\n\n##### 3.前端/客户端 封装/解析 APIJSON 的库/框架\n因为 APIJSON 基于 JSON，大部分情况下都可以直接用 fastjson 等 JSON 封装/解析库或其它工具等，<br />\n只是 APIJSON 有部分功能需要在 key 里放 [], @ 等特殊符号，返回 [] 在某些情况下不方便解析，<br />\n目前可使用 \"format\": true 让后端格式化后返回，但也会对服务器性能有一些损耗，<br />\n如果 前端/客户端 有对应的格式化工具等（例如 [apijson-orm](https://github.com/APIJSON/apijson-orm) 可供 Android 使用 format），选择就会更多一些。<br />\n\n##### ...  //欢迎补充\n\n### 推广使用\n#### 为 APIJSON 编写/发表 博客/文章/资讯 等\n\nhttps://github.com/Tencent/APIJSON#%E7%9B%B8%E5%85%B3%E6%8E%A8%E8%8D%90\n\n20200205 更新：最近首页相关推荐新增了 1 篇官方发的文章和 6 篇用户发的文章\nhttps://github.com/Tencent/APIJSON/blob/master/README.md#%E7%9B%B8%E5%85%B3%E6%8E%A8%E8%8D%90\n\n#### 登记正在使用 APIJSON 的公司或项目\n\nhttps://github.com/Tencent/APIJSON/issues/73\n\n#### 在社交技术群、论坛等聊天或评论时推荐 APIJSON\n\n#### ...  //欢迎补充\n\n\n"
  },
  {
    "path": "assets/a",
    "content": "\n"
  },
  {
    "path": "详细的说明文档.md",
    "content": "# APIJSON 入门教程\r\n\r\n可以先看更清晰直观的视频教程 <br >\r\nhttps://search.bilibili.com/all?keyword=APIJSON\r\n![image](https://user-images.githubusercontent.com/5738175/135413311-0207ec13-f7ea-4767-9e34-1a6d08438295.png)\r\n\r\n本文档已部署到官网，浏览和检索体验更好，但官网更新滞后，如有疏漏以本文档为准 <br >\r\nhttp://apijson.cn/doc/zh/\r\n\r\n其它各种官方和第三方文档见首页相关推荐 <br >\r\nhttps://github.com/Tencent/APIJSON#%E7%9B%B8%E5%85%B3%E6%8E%A8%E8%8D%90\r\n\r\n## A.介绍\r\n\r\n### A1.关于接口开发\r\n\r\n首先是看名字 `APIJSON`，API 是说这个项目是属于接口开发的项目，JSON 是指传输数据格式是 JSON 格式。介于各位看官的水平高低不齐，这里就先为没有项目经验朋友啰嗦两句接口开发的内容。有经验的朋友可以跳到 `A2` 继续查看。完整的详细介绍见项目首页 <br >\r\nhttps://github.com/Tencent/APIJSON#--apijson\r\n\r\n### A2.功能说明\r\n\r\n一个接口的开发，比如 Java 用 SpringBoot + Mybatis 来开发一般来说就像下面这个流程\r\n\r\n![1545468341131](assets/1545468341131.png)\r\n\r\n部署上这个项目后，流程变成了这样\r\n\r\n![1545468361962](assets/1545468361962.png)\r\n\r\n如果使用 [apijson-framework](https://github.com/APIJSON/apijson-framework)，还可进一步简化流程\r\n\r\n![1543975563776](https://raw.githubusercontent.com/Tencent/APIJSON/master/assets/1543975563776.png)\r\n\r\n\r\n换句话说，使用这个项目作为后端的支持的话，是不需要对每个表写增删改查等接口的，只需在该项目连接的数据里进行表的创建，以及配置接口权限即可。无需进行过多的开发，哪怕是要改结构也仅仅只需要修改表字段而已。想想仅仅是部署一个后端项目，现在需要写的接口就基本写好了，直接调用就行了，是不是挺爽的。\r\n\r\n说这么多，咱们直接开干吧！\r\n\r\n\r\n\r\n# B.安装&使用\r\n\r\n## B1.环境配置\r\n\r\nJDK: 1.8+\r\n\r\nMAVEN: 3.0+\r\n\r\nMySQL 5.7+ / PostgreSQL 9.5+ / Oracle 12C+ / ClickHouse 21.1+ / Presto 0.277+ / Hive 3.1.2+ / ...\r\n\r\nIntelliJ IDEA 2018+ / Eclipse Java EE IDE 4.5.1+\r\n\r\n## B2.下载项目\r\n\r\n```git\r\ngit clone https://github.com/APIJSON/APIJSON-Demo.git\r\n```\r\n\r\n或者，直接下载 ZIP 打包好的项目文件。\r\n\r\n![image](https://user-images.githubusercontent.com/5738175/135412855-574cae7b-402c-4fe0-9959-711e580799af.png)\r\n\r\n## B3.导入项目\r\n\r\nEclipse 导入：\r\n\r\n顶部菜单 File > Import > Maven > Existing Maven Projects > Next > Browse\r\n\r\n选择项目所在目录 [/APIJSON-Demo-Master/APIJSON-Java-Server](https://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server)/APIJSONBoot\r\n\r\n当报依赖错误的时候，将同目录下的 libs 里面的 jar 包添加到 Build Path 中。\r\n![1542345887787](assets/1542345887787.png)\r\n\r\n为了方便修改源代码，你可以像我一样不添加 libs/APIJSON.jar 文件到 Build Path 中。而是将 libs/APIJSON.jar 的源码，复制到当前项目里，然后添加到 Build Path 中。\r\n\r\n源代码在 [APIJSON-Master/APIJSON-Java-Server](https://github.com/Tencent/APIJSON)/APIJSONORM。\r\n\r\nIntelliJ IDEA 导入具体见：\r\n\r\nhttps://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server#3%E7%94%A8-intellij-idea-ultimate-%E6%88%96-eclipse-for-javaee-%E8%BF%90%E8%A1%8C%E5%90%8E%E7%AB%AF%E5%B7%A5%E7%A8%8B\r\n\r\n## B4.错误修改\r\n\r\n有可能 pom.xml 会报错，例如：\r\n\r\n```java\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n```\r\n\r\n这段代码中的这一句提示错误：\r\n\r\n```java\r\nxsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n```\r\n\r\n修改步骤：\r\n\r\n- 请修改 Eclipse 中的 Maven 镜像地址，以便更快下载或者更新，具体方法自行百度；\r\n- 打开 Eclipse->Windows->Preferences->Maven->Installations->Add 这个按钮用于指定 Maven 的安装目录。这里不建议使用 Eclipse 自带的，需要再自己设置。最终效果如下图所示：\r\n  ![install3](https://raw.githubusercontent.com/APIJSON/APIJSON-Doc/master/docs/.vuepress/public/assets/install3.png)\r\n- 打开 Eclipse->Windows->Preferences->Maven->User Settings 这是指定 setting.xml 的位置，同时导向自己的本地 maven 仓库。最终效果如下图所示：\r\n  ![install4](https://raw.githubusercontent.com/APIJSON/APIJSON-Doc/master/docs/.vuepress/public/assets/install4.png)\r\n\r\n以上截图仅为示例，实际路径请以自己设定为准。\r\n\r\n\r\n## C.开发说明\r\n\r\n### C-1.基于 MySQL 数据库的开发流程\r\n\r\n####  C-1-1.修改数据库链接\r\n\r\n如我的数据库信息是这样的\r\n\r\n| 数据库参数 | 值                  |\r\n| ---------- | ------------------- |\r\n| 地址       | localhost:3306 |\r\n| 用户       | root                |\r\n| 密码       | apijson                |\r\n| 数据库     | sys                |\r\n\r\n那么需要在 `DemoSQLConfig` 配置数据库\r\n\r\n```java\r\n\tstatic {\r\n\t\tDEFAULT_DATABASE = DATABASE_MYSQL;  // TODO 默认数据库类型，改成你自己的\r\n\t\tDEFAULT_SCHEMA = \"sys\";  // TODO 默认数据库名/模式，改成你自己的，默认情况是 MySQL: sys, PostgreSQL: public, SQL Server: dbo, Oracle: \r\n\t}\r\n\t\r\n\t@Override\r\n\tpublic String getDBVersion() {\r\n\t\treturn \"5.7.22\";  // \"8.0.11\";  // TODO 改成你自己的 MySQL 或 PostgreSQL 数据库版本号  // MYSQL 8 和 7 使用的 JDBC 配置不一样\r\n\t}\r\n\t\r\n\t@JSONField(serialize = false)  // 不在日志打印 账号/密码 等敏感信息\r\n\t@Override\r\n\tpublic String getDBUri() {\r\n\t\t// 这个是 MySQL 8.0 及以上，要加 userSSL=false  return \"jdbc:mysql://localhost:3306?userSSL=false&serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8\";\r\n\t\t// 以下是 MySQL 5.7 及以下\r\n\t\treturn \"jdbc:mysql://localhost:3306?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8\"; //TODO 改成你自己的，TiDB 可以当成 MySQL 使用，默认端口为 4000\r\n\t}\r\n\t\r\n\t@JSONField(serialize = false)  // 不在日志打印 账号/密码 等敏感信息\r\n\t@Override\r\n\tpublic String getDBAccount() {\r\n\t\treturn \"root\";  // TODO 改成你自己的\r\n\t}\r\n\t\r\n\t@JSONField(serialize = false)  // 不在日志打印 账号/密码 等敏感信息\r\n\t@Override\r\n\tpublic String getDBPassword() {\r\n\t\treturn \"apijson\";  // TODO 改成你自己的，TiDB 可以当成 MySQL 使用， 默认密码为空字符串 \"\"\r\n\t}\r\n```\r\n具体见源码 <br >\r\nhttps://github.com/APIJSON/APIJSON-Demo/blob/master/APIJSON-Java-Server/APIJSONDemo/src/main/java/apijson/demo/DemoSQLConfig.java\r\n\r\n#### C-1-2.导入表\r\n\r\n在 [APIJSON-Demo-Master/MySQL](https://github.com/APIJSON/APIJSON-Demo/tree/master/MySQL) 目录下有一批 SQL 脚本，他们看起来是这样的\r\n\r\n![1542345654422](assets/1542345654422.png)\r\n\r\nMySQLWorkbench/Datagrip/Navicat 导入表具体见：\r\n\r\nhttps://github.com/APIJSON/APIJSON-Demo/tree/master/APIJSON-Java-Server#2%E5%AF%BC%E5%85%A5%E8%A1%A8%E6%96%87%E4%BB%B6%E5%88%B0%E6%95%B0%E6%8D%AE%E5%BA%93\r\n\r\n导入完成之后。我们可以把项目跑起来看下，以刚刚配置的项目，项目是否能够连上数据库。其中也有一些初始化数据，可以方便我们测试。\r\n\r\n### C-1-2-1.更多测试用例\r\n\r\n如果需要更多测试用例，请按照以下步骤获取：\r\n1. 用 Chrome(或 Firefox 等)浏览器中打开 http://apijson.cn/api ，或者 http://apijson.cn 然后点击按钮 \\[接口 APIAuto 测试] 也行；\r\n2. 点击右上角的“登录”按钮登录；\r\n3. 点击“测试账号”按钮左边第二个按钮，（也就是“-”左边的第一个）获取各种测试用例；\r\n4. 欢迎大家踊跃共享自己的测试用例；\r\n\r\n也可以参考 APIAuto 的文档或视频：\r\n\r\nhttps://github.com/TommyLemon/APIAuto\r\n\r\n#### C-1-3.测试查询\r\n\r\n为了方便测试，我这里使用的 Chrome 浏览器的 Restlet Client 插件，大家可以根据自己的喜好使用不同的工具测试。\r\n\r\n使用 `http://localhost:8080/get` 测试结果。（请注意 DemoApplication.java 中使用 Tomcat 默认的 8080 端口，如果不小心开着PC端的“微信”，8080 端口会被占用，建议改成 8081, 9090 等其它应用程序未占用的端口。）\r\n\r\n随便找一个表，比如 `Moment` 表，我们取其中 id 为 12 的一条出来看看\r\n\r\n![1542350018926](assets/1542350018926.png)\r\n\r\n对接口地址 `http://localhost:8080/get` 发送一个 JSON 格式的请求\r\n\r\n![1542350219020](assets/1542350219020.png)\r\n\r\n请求的JSON:\r\n\r\n```json\r\n{\r\n  \"Moment\": {\r\n    \"id\":12\r\n  }\r\n}\r\n```\r\n\r\n响应的JSON：\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"content\": \"1111534034\",\r\n        \"date\": \"2017-02-08 16:06:11.0\",\r\n        \"id\": 12,\r\n        \"pictureList\": [\r\n            \"http://static.oschina.net/uploads/img/201604/22172508_eGDi.jpg\",\r\n            \"http://static.oschina.net/uploads/img/201604/22172507_rrZ5.jpg\"\r\n        ],\r\n        \"praiseUserIdList\": [\r\n            70793,\r\n            93793,\r\n            82001\r\n        ],\r\n        \"userId\": 70793\r\n    },\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n\r\n\r\n##### 字段过滤\r\n\r\n这里这么多字段，如果我只想要这个 `content` 字段的信息怎么办？\r\n\r\n你可以这样请求：\r\n\r\n```json\r\n{\r\n  \"Moment\": {\r\n    \"id\":12,\r\n    \"@column\":\"content\"\r\n  }\r\n}\r\n```\r\n\r\n响应：\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"content\": \"1111534034\"\r\n    },\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n`@column` 表示你要筛选出的字段，如果是多个字段可以这样写 `\"@column\":\"id,date,content\"`\r\n\r\n\r\n\r\n##### 字段别名\r\n\r\n如果想要使用字段的别名应该这样写：`\"@column\":\"id,date:time,content:text\"`\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"text\": \"1111534034\",\r\n        \"time\": \"2017-02-08 16:06:11.0\",\r\n        \"id\": 12\r\n    }\r\n}\r\n```\r\n\r\n这样在返回的数据中 `date` 字段就变成了 `time`, `content` 字段变成了 `text`。\r\n\r\n\r\n\r\n##### 逻辑运算\r\n\r\n如果想要筛选出，id 在 `12,15,32` 中的这三条数据的`日期`和`内容`怎么办呢？\r\n\r\n请求：\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"id{}\":[12,15,32],\r\n        \"@column\":\"id,date,content\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n响应：\r\n\r\n```json\r\n{\r\n    \"[]\": [\r\n        {\r\n            \"Moment\": {\r\n                \"content\": \"1111534034\",\r\n                \"date\": \"2017-02-08 16:06:11.0\",\r\n                \"id\": 12\r\n            }\r\n        },\r\n        {\r\n            \"Moment\": {\r\n                \"content\": \"APIJSON is a JSON Transmission Structure Protocol…\",\r\n                \"date\": \"2017-02-08 16:06:11.0\",\r\n                \"id\": 15\r\n            }\r\n        },\r\n        {\r\n            \"Moment\": {\r\n                \"date\": \"2017-02-08 16:06:11.0\",\r\n                \"id\": 32\r\n            }\r\n        }\r\n    ],\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n\r\n\r\n如果所要筛选的数据的是在一定范围内的，比如 id 是 300 到 400 之间的\r\n\r\n你可以这样过滤 `\"id&{}\":\">=300,<=400\"`\r\n\r\n&的作用是表明后面的两个过滤条件的逻辑关系\r\n\r\n```sql\r\n(id >= 300 AND id <= 500)\r\n```\r\n\r\n\r\n\r\n现在的逻辑符号一共有三种，`&`，`|`，`!`\r\n\r\n默认的逻辑关系是 `|`，也就是说 `\"id|{}\":\"<=300,>=400\"` 和 `\"id{}\":\"<=300,>=400\"` 等价。\r\n\r\n`!`主要用于反选，黑名单之类的\r\n\r\n`\"id!{}\":[12,15,32]` 表示 `id` 不在 12，15，32 内的其他数据。\r\n\r\n复杂一些，如果多个条件相互组合，可以写多个关于id的过滤条件\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"id&{}\":\">=10,<=40\",\r\n        \"id!{}\":[12],\r\n        \"@column\":\"id,date,content:text\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n比如这里表示 id 在 10 到 40 之间，但是却不包含 12 的数据。\r\n\r\n\r\n\r\n##### 模糊查询\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"content$\":\"%APIJSON%\",\r\n        \"@column\":\"id,date,content:text\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n使用方式有多种：\r\n\r\n`keyword%`，以 `keyword` 开头的字符串。\r\n\r\n`%keyword`，以 `keyword` 结束的字符串。\r\n\r\n`%keyword%`，包含 `keyword` 的字符串，如：`keyword123`, `123keyword`, `123keyword123`\r\n\r\n`%k%e%y%`，包含字母 `k`, `e`, `y` 的字符串\r\n\r\n还有几种比较便捷的方式，我们这里如果使用 `\"content~\":\"keyword\"` 来代替 `\"content$\":\"%keyword%\"`，同样可以表示包含某字符串\r\n\r\n\r\n\r\n##### 正则匹配\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"content~\":\"^[0-9]+$\",\r\n        \"@column\":\"id,date,content:text\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n正则表达式 `^[0-9]+$`，查询 `content` 为纯数字的数据，[MySQL 的正则语法](http://www.runoob.com/mysql/mysql-regexp.html) 如下：\r\n\r\n| 模式       | 描述                                                         |\r\n| ---------- | ------------------------------------------------------------ |\r\n| ^          | 匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性，^ 也匹配 '\\n' 或 '\\r' 之后的位置。 |\r\n| $          | 匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性，$ 也匹配 '\\n' 或 '\\r' 之前的位置。 |\r\n| .          | 匹配除 \"\\n\" 之外的任何单个字符。要匹配包括 '\\n' 在内的任何字符，请使用象 '[.\\n]' 的模式。 |\r\n| [...]      | 字符集合。匹配所包含的任意一个字符。例如， '[abc]' 可以匹配 \"plain\" 中的 'a'。 |\r\n| [^...]     | 负值字符集合。匹配未包含的任意字符。例如， '[^abc]' 可以匹配 \"plain\" 中的'p'。 |\r\n| p1\\|p2\\|p3 | 匹配 p1 或 p2 或 p3。例如，'z\\|food' 能匹配 \"z\" 或 \"food\"。'(z\\|f)ood' 则匹配 \"zood\" 或 \"food\"。 |\r\n| *          | 匹配前面的子表达式零次或多次。例如，zo* 能匹配 \"z\" 以及 \"zoo\"。* 等价于{0,}。 |\r\n| +          | 匹配前面的子表达式一次或多次。例如，'zo+' 能匹配 \"zo\" 以及 \"zoo\"，但不能匹配 \"z\"。+ 等价于 {1,}。 |\r\n| {n}        | n 是一个非负整数。匹配确定的 n 次。例如，'o{2}' 不能匹配 \"Bob\" 中的 'o'，但是能匹配 \"food\" 中的两个 o。 |\r\n| {n,m}      | m 和 n 均为非负整数，其中n <= m。最少匹配 n 次且最多匹配 m 次。 |\r\n\r\n\r\n\r\n##### 列表数据\r\n\r\n之前我们看到返回的数据是这样的\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"content\": \"1111534034\",\r\n        \"date\": \"2017-02-08 16:06:11.0\",\r\n        \"id\": 12,\r\n        \"pictureList\": [\r\n            \"http://static.oschina.net/uploads/img/201604/22172508_eGDi.jpg\",\r\n            \"http://static.oschina.net/uploads/img/201604/22172507_rrZ5.jpg\"\r\n        ],\r\n        \"praiseUserIdList\": [\r\n            70793,\r\n            93793,\r\n            82001\r\n        ],\r\n        \"userId\": 70793\r\n    },\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n里面的 `pictureList` 和 `praiseUserIdList` 是数组，这种数据在 MySQL 数据库中是 JSON 数据格式的。\r\n\r\n![1542357146401](assets/1542357146401.png)\r\n\r\n数据库里存储的值是这样的\r\n\r\n![1542357265371](assets/1542357265371.png)\r\n\r\n如果我们想过滤出里面有 `82001` 的数据，我们应该这样请求\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"praiseUserIdList<>\":82001,\r\n        \"@column\":\"id,date,content,praiseUserIdList\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n结果是类似这样的，为了显示方便剔除了一些数据。\r\n\r\n```js\r\n{\r\n    \"[]\": [ // JavaScript/TypeScript/Python 中可用 data[\"[]\"] 来提取不符合变量名格式的 key 对应的 value，Java 等可以用 data.getJSONArray(\"[]\") 等。\r\n        \r\n        {\r\n            \"Moment\": {\r\n                \"date\": \"2017-02-08 16:06:11.0\",\r\n                \"id\": 32,\r\n                \"praiseUserIdList\": [\r\n                    38710,\r\n                    82002,\r\n                    82001\r\n                ]\r\n            }\r\n        },\r\n        {\r\n            \"Moment\": {\r\n                \"content\": \"This is a Content...-435\",\r\n                \"date\": \"2017-02-01 19:14:31.0\",\r\n                \"id\": 58,\r\n                \"praiseUserIdList\": [\r\n                    38710,\r\n                    82003,\r\n                    82001\r\n                ]\r\n            }\r\n        },\r\n        {\r\n            \"Moment\": {\r\n                \"content\": \"https://gss2.bdstatic.com/-fo3dSag_xIb.jpg\",\r\n                \"date\": \"2018-10-27 17:58:02.0\",\r\n                \"id\": 1540634282433,\r\n                \"praiseUserIdList\": [\r\n                    82001\r\n                ]\r\n            }\r\n        }\r\n    ],\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n\r\n\r\n##### 分页\r\n\r\n对于数量太多的数据，我们很多时候都需要分页操作，这时候我们可以用类似下面这样的请求\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"@column\":\"id,date,content,praiseUserIdList\"\r\n      },\r\n    \"page\": 0,\r\n    \"count\": 5\r\n  }\r\n}\r\n```\r\n\r\n请注意，这里的 `page` 和 `count` 是放在 `[] `内的属性，而不是 `Moment` 对象里。这里 `count` 表示每页的数量，`page` 表示第几页，页数从 0 开始算。\r\n\r\n也许你想看看这个请求对应的SQL语句\r\n\r\n```sql\r\nSELECT `id`,`date`,`content`,`praiseUserIdList` FROM `sys`.`Moment` LIMIT 5 OFFSET 0\r\n```\r\n\r\n这里`sys`是我自己的`schema`的名字，你的可能会有所不同。\r\n\r\n如果不想分页的，也提供了一套特殊的查询方式。这种查询方式有三种，请求方式类型这样\r\n\r\n```js\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"@column\":\"id,date,content,praiseUserIdList\"\r\n      },\r\n      \"query\": 2\r\n  },\r\n  \"total@\":\"/[]/total\" // key 里的 total 可改成 num 等几乎任意其它名称，还可以用 \"info@\": \"/[]/info\" 获取更详细的分页信息\r\n}\r\n```\r\n\r\n这里因为 `query` 的值是 2，所有会同时查询 `Moment` 表中的数据和总数。如果是 1 的话，则只会返回当前表的总数：\r\n\r\n```json\r\n{\"total\":59,\"code\":200,\"msg\":\"success\"}\r\n```\r\n\r\n\r\n\r\n数据库中的数量：\r\n\r\n![1544515879364](assets/1544515879364.png)\r\n\r\n\r\n\r\n当然，如果你添加了过滤条件，返回的数量就会是你所过滤的数量，比如：\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"@column\":\"id,date,content,praiseUserIdList\",\r\n        \"praiseUserIdList<>\":38710\r\n      },\r\n      \"query\": 1\r\n  },\r\n  \"total@\":\"/[]/total\"\r\n}\r\n```\r\n\r\n返回：\r\n\r\n```json\r\n{\"total\":12,\"code\":200,\"msg\":\"success\"}\r\n```\r\n\r\n\r\n\r\n##### 排序\r\n\r\n要使用排序的话，这样操作\r\n\r\n```json\r\n{\r\n  \"[]\": {\r\n      \"Moment\":{\r\n        \"@column\":\"id,date,content,praiseUserIdList\",\r\n        \"praiseUserIdList<>\":38710,\r\n        \"@order\":\"date-,id,content+\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n`\"@order\": \"date-,id,content+\"` 其中，字段的前后顺序表示字段排序的优先级。`id` 和 `id+` 是等价的，默认就是升序排列。`date-` 表示将 `date` 字段降序排列。\r\n\r\n##### 关联查询\r\n\r\n在讲解关联查询的时候，我们需要先了解下表之间的关系\r\n\r\n现在有两张表 Moment 和 User，两张表的关系是下面这样\r\n\r\n![1545468294295](assets/1545468294295.png)\r\n\r\nMOMENT表示动态，类似微信朋友圈、QQ 空间的动态，每一条动态会有一个发表动态的用户 User，所以 Moment 表里会有一个和 User 表的外键关联：Moment.userId = User.id。\r\n\r\n对于这样的数据关系，我们在查询动态时，很多时候我们会连带着用户一起查处来，这样又如何操作呢\r\n\r\n```js\r\n{\r\n  \"[]\": {\r\n    \"Moment\":{\r\n        \"@column\":\"id,date,userId\",\r\n        \"id\":12 // 注意 id 是主键，这个数组最多返回 1 条子项（如果 id=12 的表记录存在）\r\n    },\r\n    \"User\":{\r\n      \"id@\":\"/Moment/userId\", // 不要求物理外键，只要能关联即可\r\n      \"@column\":\"id,name\"\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n这个请求稍微复杂点，首先我们用 `[]` 对象表示我们是想查询出一个列表，这个列表包含两个部分 `Moment` 和 `User`。\r\n\r\n其中 `Moment` 是我们想要查询的主要内容，它的写法也和一般查询数据时无异。\r\n\r\n`User` 是与 `Moment` 相关联的数据，所以查询的时候我们需要用 `id@` 来表示他们之间的关联关系\r\n\r\n`/Moment/userId` 中，最开始的 `/` 相当于是指明了 `[]` 的位置，`/Moment` 表示 `[]` 对象下的 `Moemnt` 对象，`/Moment/userId` 表示 `Moment` 的 `userId` 字段是与 `User` 的 `id` 关联的。这是一种缺省引用路径，这里等价于完整引用路径 `[]/Moment/userId`。\r\n\r\n响应的数据：\r\n\r\n```json\r\n{\r\n    \"[]\": [\r\n        {\r\n            \"Moment\": {\r\n                \"date\": \"2017-02-08 16:06:11.0\",\r\n                \"id\": 12,\r\n                \"userId\": 70793\r\n            },\r\n            \"User\": {\r\n                \"id\": 70793,\r\n                \"name\": \"Strong\"\r\n            }\r\n        }\r\n    ],\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n\r\n\r\n##### 分组查询\r\n\r\n在了解分组查询之前，我们需要先了解下 APIJSON 所支持的部分常见函数：\r\n\r\n| 函数名 | 说明                       |\r\n| ------ | -------------------------- |\r\n| count  | 统计分组下，某字段的个数   |\r\n| sum    | 统计分组下，某字段的和     |\r\n| max    | 统计分组下，某字段的最大值 |\r\n| min    | 统计分组下，某字段的最小值 |\r\n| avg    | 统计分组下，某字段的平均值 |\r\n\r\n具体见 AbstractSQLConfig 中 SQL_FUNCTION_MAP 配置的所有 SQL 函数 ：\r\n\r\nhttps://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java#L286-L765\r\n\r\n比如，如果只是单纯的查出最大值，这样请求就可以了\r\n\r\n```json\r\n{\r\n  \"Moment\":{\r\n     \"@column\":\"max(id):maxid\"\r\n  }\r\n}\r\n```\r\n\r\n响应：\r\n\r\n```json\r\n{\r\n  \"Moment\": {\r\n     \"maxid\": 1541912160047\r\n  },\r\n  \"code\": 200,\r\n  \"msg\": \"success\"\r\n}\r\n```\r\n\r\n这里 `maxid` 是我们取的别名\r\n\r\n如果是有分组条件的，那我们需要使用 `@group`\r\n\r\n比如，像下面 Sale 表，这张表表示，2018年1月1日某公司门下的 3 个店铺（store_id）的营业额（amt）数据\r\n\r\n| id   | store_id | amt  |\r\n| ---- | -------- | ---- |\r\n| 1    | 1        | 100  |\r\n| 2    | 1        | 80   |\r\n| 3    | 2        | 30   |\r\n| 4    | 2        | 100  |\r\n| 5    | 3        | 210  |\r\n\r\n如果，我们想要计算出这天每个店铺一共卖了多少，我们通过 APIJSON 可以这样查询\r\n\r\n```\r\n{\r\n  \"[]\": {\r\n      \"Sale\":{\r\n        \"@column\":\"store_id;sum(amt):totAmt\", // 注意 SQL 函数要用分号 ; 隔开\r\n        \"@group\":\"store_id\"\r\n      }\r\n  }\r\n}\r\n```\r\n\r\n#### C-1-4.登录\r\n\r\n如果没有登录，由于权限的限制，是需要登录的。\r\n\r\n登录地址 `http://localhost:8080/login` 或 `http://127.0.0.1:8080/login`，发送请求\r\n\r\n```json\r\n{\r\n  \"phone\": \"13000038710\",\r\n  \"password\":\"apijson\"\r\n}\r\n```\r\n\r\n账号和密码，可以到 `apijson_privacy` 里面查询\r\n\r\n#### C-1-5.测试新增\r\n\r\n接口地址：`http://localhost:8080/post`\r\n\r\n我们想新增一条备注时，发送这样的请求\r\n\r\n请求\r\n\r\n```json\r\n{\r\n \"Moment\":{\r\n      \"content\":\"今天天气不错，到处都是提拉米苏雪\",\r\n      \"userId\":38710\r\n },\r\n  \"tag\":\"Moment\"\r\n}\r\n```\r\n\r\n`tag` 是我们在 `Request` 表里面配置的 `tag` 字段。\r\n\r\n响应\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"code\": 200,\r\n        \"count\": 1,\r\n        \"id\": 1544520921923,\r\n        \"msg\": \"success\"\r\n    },\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n返回的 `id` 是新增的数据的新主键值\r\n\r\n#### C-1-4.测试修改\r\n\r\n接口地址：`http://localhost:8080/put`\r\n\r\n修改备注和新增类似\r\n\r\n请求\r\n\r\n```json\r\n{\r\n \"Moment\":{\r\n      \"id\":1544520921923,\r\n      \"content\":\"海洋动物数量减少，如果非吃不可，不点杀也是在保护它们\"\r\n },\r\n  \"tag\":\"Moment\"\r\n}\r\n```\r\n\r\n响应\r\n\r\n```json\r\n{\r\n    \"Moment\": {\r\n        \"code\": 200,\r\n        \"count\": 1,\r\n        \"id\": 1544520921923,\r\n        \"msg\": \"success\"\r\n    },\r\n    \"code\": 200,\r\n    \"msg\": \"success\"\r\n}\r\n```\r\n\r\n如果要对 `json` 类型操作的话，这样请求\r\n\r\n```json\r\n{\r\n \"Moment\":{\r\n      \"id\":1544520921923,\r\n      \"praiseUserIdList+\": [123]\r\n },\r\n  \"tag\":\"Moment\"\r\n}\r\n```\r\n\r\n这里的 `praiseUserIdList` 是一个 `json` 类型的字段，在操作之前它是空的 `[]`，提交以后它是 `[123]`，如果再添加一个 21，则会变成 `[123,21]`\r\n\r\n要删除其中的值，把 `+` 变成 `-` 即可。如果没有 `+`, `-` 符号，\"praiseUserIdList\": \\[123] 将直接替换原本的值，可能丢失数据。\r\n\r\n#### C-1-5.测试删除\r\n\r\n接口地址：`http://localhost:8080/delete`\r\n\r\n请求\r\n\r\n```json\r\n{\r\n \"Moment\":{\r\n      \"id\":1544520921923\r\n },\r\n  \"tag\":\"Moment\"\r\n}\r\n```\r\n\r\n### C-2.新增接口\r\n\r\n#### 1. 后台添加数据表\r\n\r\n在自己的数据库里新增一个表，比如我这里新增 `b_stone`\r\n\r\n```sql\r\n-- 原石\r\nCREATE TABLE `b_stone` (\r\n  `id` bigint(20) NOT NULL AUTO_INCREMENT,\r\n  `cost` int(10) NULL COMMENT '成本',\r\n  `price` int(10) NULL COMMENT '卖价',\r\n  `length` int(10) NULL,\r\n  `width`  int(10) NULL,\r\n  `height` int(10) NULL,\r\n  `weight` float(8,1) NULL,\r\n  `creationdate` datetime default CURRENT_TIMESTAMP COMMENT '创建时间',\r\n  `modifydate` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\r\n  `modifier` varchar(80) NULL,\r\n  PRIMARY KEY (`id`)\r\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;\r\n```\r\n\r\n#### 2. 配置权限\r\n\r\nAPIJSON 3.7.0  版开始，依赖了 apijson-framework.jar 的不需要写任何代码：\r\n\r\n##### 2.1）在 Access 表里加一行记录即可\r\n\r\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_access_config-small.jpg) \r\n\r\n<br />\r\n<br />\r\n\r\n~~如果低于 3.7.0 或者未依赖 apijson-framework.jar，而是直接依赖 apijson-orm.jar，则需要编写代码：~~\r\n\r\n##### ~~2.1）在 Model 中添加对象并配置权限~~\r\n\r\n~~项目的 model 目录下，新增一个类~~\r\n\r\n```java\r\npackage apijson.demo.server.model;\r\n\r\nimport zuo.biao.apijson.MethodAccess;\r\n\r\n@MethodAccess\r\npublic class Stone {\r\n}\r\n```\r\n\r\n~~注解`@MethodAccess`的配置，可以参考其他类~~\r\n\r\n\r\n\r\n~~由于我们的类名和数据库表名不一致，需要注册一下。如果一样就不需要了。~~\r\n\r\n~~设置数据库的实际表名 `DemoSQLConfig`，38 行~~\r\n\r\n```java\r\n//表名映射，隐藏真实表名，对安全要求很高的表可以这么做\r\n\tstatic {\r\n\t\tTABLE_KEY_MAP.put(User.class.getSimpleName(), \"apijson_user\");\r\n\t\tTABLE_KEY_MAP.put(Privacy.class.getSimpleName(), \"apijson_privacy\");\r\n\t\tTABLE_KEY_MAP.put(Stone.class.getSimpleName(), \"b_stone\"); // <--这一句\r\n\t}\r\n```\r\n\r\n\r\n\r\n~~注册权限是必须的，这样程序才能使用你配置的类权限去管理你的接口~~\r\n\r\n~~脚本 `DemoVerifier.java` 的 48 行~~\r\n\r\n```java\r\nstatic { //注册权限\r\n\t\tACCESS_MAP.put(User.class.getSimpleName(), getAccessMap(User.class.getAnnotation(MethodAccess.class)));\r\n....\r\n\t\tACCESS_MAP.put(Stone.class.getSimpleName(), getAccessMap(Stone.class.getAnnotation(MethodAccess.class)));\r\n\t}\r\n```\r\n\r\n<br />\r\n<br />\r\n\r\n#### 3. 接口管理Request表的配置\r\n![](https://raw.githubusercontent.com/TommyLemon/StaticResources/master/APIJSON/APIJSON_request_config-small.jpg) \r\n可这样设置 structure 字段来配置自动校验请求 JSON 参数： <br />\r\n```json\r\n\"VERIFY\":{\r\n  \"type{}\":[0,1,2]\r\n}\r\n```\r\n就能校验 type 的值是不是 0，1，2 中的一个。<br />\r\n还有 <br />\r\n```js\r\n\"VERIFY\": { \"money&{}\":\">0,<=10000\" }              //自动验证是否 money>0 & money<=10000\r\n\"TYPE\": { \"balance\": \"DECIMAL\" }                    //自动验证 balance 类型是否为 DECIMAL（对应 Double 双精度浮点数）\r\n\"UNIQUE\": \"phone\"                                  //强制 phone 的值为数据库中没有的\r\n\"NECESSARY\": \"id,name\"                             //强制传 id,name 两个字段\r\n\"DISALLOW\": \"balance\"                              //禁止传 balance 字段\r\n\"INSERT\": { \"@role\": \"OWNER\" }                     //如果没传 @role 就自动添加\r\n\"UPDATE\": { \"id@\": \"User/id\" }                     //强制放入键值对\r\n```\r\n全部操作符见 [Operation.java](https://github.com/Tencent/APIJSON/blob/master/APIJSONORM/src/main/java/apijson/orm/Operation.java) 的注释\r\n<br />\r\n<br />\r\n\r\n\r\n:first_quarter_moon_with_face:此处的介绍都只是简要介绍，只是为了引导刚刚接触 APIJSON 的道友快速了解 APIJSON，并不代表 APIJSON 只有这些功能，具体功能详情参考下列图表\r\n\r\n#### 4. 完整功能图表\r\nhttps://github.com/Tencent/APIJSON/blob/master/Document.md#3\r\n\r\n\r\n"
  }
]