diff --git a/APIJSONORM/src/main/java/apijson/JSONObject.java b/APIJSONORM/src/main/java/apijson/JSONObject.java index 6825a02de..db21693cf 100755 --- a/APIJSONORM/src/main/java/apijson/JSONObject.java +++ b/APIJSONORM/src/main/java/apijson/JSONObject.java @@ -151,6 +151,7 @@ public JSONObject setUserIdIn(List list) { public static final String KEY_ORDER = "@order"; //排序方式 public static final String KEY_RAW = "@raw"; // 自定义原始 SQL 片段 public static final String KEY_JSON = "@json"; //SQL Server 把字段转为 JSON 输出 + public static final String KEY_METHOD = "@method"; //json对象配置操作方法 public static final List TABLE_KEY_LIST; static { diff --git a/APIJSONORM/src/main/java/apijson/StringUtil.java b/APIJSONORM/src/main/java/apijson/StringUtil.java index 795392e49..67ca446f6 100755 --- a/APIJSONORM/src/main/java/apijson/StringUtil.java +++ b/APIJSONORM/src/main/java/apijson/StringUtil.java @@ -354,7 +354,7 @@ public static boolean isNotEmpty(String s, boolean trim) { PATTERN_ALPHA = Pattern.compile("^[a-zA-Z]+$"); PATTERN_ALPHA_BIG = Pattern.compile("^[A-Z]+$"); PATTERN_ALPHA_SMALL = Pattern.compile("^[a-z]+$"); - PATTERN_NAME = Pattern.compile("^[0-9a-zA-Z_]+$");//已用55个中英字符测试通过 + PATTERN_NAME = Pattern.compile("^[0-9a-zA-Z_:]+$");//已用55个中英字符测试通过 //newest phone regex expression reference https://github.com/VincentSit/ChinaMobilePhoneNumberRegex PATTERN_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}$"); PATTERN_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})(\\]?)$"); diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java index e99908024..41b13c491 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java @@ -47,7 +47,7 @@ */ public abstract class AbstractParser implements Parser, ParserCreator, VerifierCreator, SQLCreator { protected static final String TAG = "AbstractParser"; - + protected Map key_method_Map = new HashMap<>(); /** * 可以通过切换该变量来控制是否打印关键的接口请求内容。保守起见,该值默认为false。 * 与 {@link Log#DEBUG} 任何一个为 true 都会打印关键的接口请求内容。 @@ -572,28 +572,11 @@ public JSONObject parseCorrectRequest(RequestMethod method, String tag, int vers return request;//需要指定JSON结构的get请求可以改为post请求。一般只有对安全性要求高的才会指定,而这种情况用明文的GET方式几乎肯定不安全 } - if (StringUtil.isEmpty(tag, true)) { - throw new IllegalArgumentException("请在最外层传 tag !一般是 Table 名,例如 \"tag\": \"User\" "); - } - - //获取指定的JSON结构 <<<<<<<<<<<< - JSONObject object = null; - String error = ""; - try { - object = getStructure("Request", method.name(), tag, version); - } catch (Exception e) { - error = e.getMessage(); - } - if (object == null) { //empty表示随意操作 || object.isEmpty()) { - throw new UnsupportedOperationException("找不到 version: " + version + ", method: " + method.name() + ", tag: " + tag + " 对应的 structure !" - + "非开放请求必须是后端 Request 表中校验规则允许的操作!\n " + error + "\n如果需要则在 Request 表中新增配置!"); - } - - //获取指定的JSON结构 >>>>>>>>>>>>>> - JSONObject target = wrapRequest(method, tag, object, true); +// if (StringUtil.isEmpty(tag, true)) { +// throw new IllegalArgumentException("请在最外层传 tag !一般是 Table 名,例如 \"tag\": \"User\" "); +// } - //JSONObject clone 浅拷贝没用,Structure.parse 会导致 structure 里面被清空,第二次从缓存里取到的就是 {} - return getVerifier().verifyRequest(method, name, target, request, maxUpdateCount, getGlobalDatabase(), getGlobalSchema(), creator); + return batchVerify(method, tag, version, name, request, maxUpdateCount, creator); } @@ -1047,6 +1030,8 @@ public JSONObject onObjectParse(final JSONObject request if (op == null) { op = createObjectParser(request, parentPath, arrayConfig, isSubquery, isTable, isArrayMainTable); } + // 对象 - 设置 method + setOpMethod(request, op, name); op = op.parse(name, isReuse); JSONObject response = null; @@ -2022,7 +2007,8 @@ protected void onBegin() { */ protected void onCommit() { // Log.d(TAG, "onCommit >>"); - if (RequestMethod.isQueryMethod(requestMethod)) { + // this.sqlExecutor.getTransactionIsolation() 只有json第一次执行才会设置, get请求=0 + if (RequestMethod.isQueryMethod(requestMethod) && this.sqlExecutor.getTransactionIsolation() == Connection.TRANSACTION_NONE ) { return; } @@ -2068,4 +2054,163 @@ protected void onClose() { queryResultMap = null; } + private void setOpMethod(JSONObject request,ObjectParser op, String key) { + if(key != null && request.getString(apijson.JSONObject.KEY_METHOD) != null) { + String _method = request.getString(apijson.JSONObject.KEY_METHOD); + if( _method != null) { + RequestMethod method = RequestMethod.valueOf(_method.toUpperCase()); + this.setMethod(method); + op.setMethod(method); + } + } + } + + protected JSONObject getRequestStructure(RequestMethod method, String tag, int version) throws Exception { + // 获取指定的JSON结构 <<<<<<<<<<<< + JSONObject object = null; + String error = ""; + try { + object = getStructure("Request", method.name(), tag, version); + } catch (Exception e) { + error = e.getMessage(); + } + if (object == null) { // empty表示随意操作 || object.isEmpty()) { + throw new UnsupportedOperationException("找不到 version: " + version + ", method: " + method.name() + ", tag: " + tag + " 对应的 structure !" + "非开放请求必须是后端 Request 表中校验规则允许的操作!\n " + error + "\n如果需要则在 Request 表中新增配置!"); + } + return object; + } + + private JSONObject batchVerify(RequestMethod method, String tag, int version, String name, @NotNull JSONObject request, int maxUpdateCount, SQLCreator creator) throws Exception { + JSONObject jsonObject = new JSONObject(true); + if (request.keySet() == null || request.keySet().size() == 0) { + throw new IllegalArgumentException("json对象格式不正确 !,例如 \"User\": {}"); + } + + for (String key : request.keySet()) { + // key重复直接抛错(xxx:alias, xxx:alias[]) + if (jsonObject.containsKey(key) || jsonObject.containsKey(key + apijson.JSONObject.KEY_ARRAY)) { + throw new IllegalArgumentException("对象名重复,请添加别名区分 ! ,重复对象名为: " + key); + } + + // @post、@get等RequestMethod + try { + if (key.startsWith("@")) { + try { + // 如果不匹配,不处理即可 + RequestMethod l_method = RequestMethod.valueOf(key.substring(1).toUpperCase()); + if (l_method != null) { + if (request.get(key) instanceof JSONArray) { + for (Object objKey : request.getJSONArray(key)) { + key_method_Map.put(objKey, l_method); + } + continue; + } else { + throw new IllegalArgumentException("参数 " + key + " 必须是数组格式 ! ,例如: [\"Moment\", \"Comment[]\"]"); + } + } + } catch (Exception e) { + } + } + + // 如果对象设置了@method, 优先使用 对象内部的@method + // 对于没有显式声明操作方法的,直接用 URL(/get, /post 等) 对应的默认操作方法 + // 将method 设置到每个object, op执行会解析 + if (request.get(key) instanceof JSONObject) { + if(request.getJSONObject(key).getString(apijson.JSONObject.KEY_METHOD) == null) { + if (key_method_Map.get(key) == null) { + // 数组会解析为对象进行校验,做一下兼容 + if(key_method_Map.get(key + apijson.JSONObject.KEY_ARRAY) == null) { + request.getJSONObject(key).put(apijson.JSONObject.KEY_METHOD, method); + }else { + request.getJSONObject(key).put(apijson.JSONObject.KEY_METHOD, key_method_Map.get(key + apijson.JSONObject.KEY_ARRAY)); + } + } else { + request.getJSONObject(key).put(apijson.JSONObject.KEY_METHOD, key_method_Map.get(key)); + } + } + + // get请求不校验 + RequestMethod _method = RequestMethod.valueOf(request.getJSONObject(key).getString(apijson.JSONObject.KEY_METHOD).toUpperCase()); + if (RequestMethod.isPublicMethod(_method)) { + jsonObject.put(key, request.getJSONObject(key)); + continue; + } + } + + if (key.startsWith("@") || key.endsWith("@")) { + jsonObject.put(key, request.get(key)); + continue; + } + + + if (request.get(key) instanceof JSONObject || request.get(key) instanceof JSONArray) { + RequestMethod _method = null; + if (request.get(key) instanceof JSONObject) { + _method = RequestMethod.valueOf(request.getJSONObject(key).getString(apijson.JSONObject.KEY_METHOD).toUpperCase()); + } else { + if (key_method_Map.get(key) == null) { + _method = method; + } else { + _method = key_method_Map.get(key); + } + } + + String _tag = buildTag(request, key); + JSONObject requestItem = new JSONObject(); + requestItem.put(_tag, request.get(key)); + JSONObject object = getRequestStructure(_method, _tag, version); + JSONObject ret = objectVerify(_method, _tag, version, name, requestItem, maxUpdateCount, creator, object); + jsonObject.put(key, ret.get(_tag)); + } else { + jsonObject.put(key, request.get(key)); + } + } catch (Exception e) { + e.printStackTrace(); + throw new Exception(e); + } + } + + return jsonObject; + } + + /** + * { "xxx:aa":{ "@tag": "" }} + * 生成规则: + * 1、@tag存在,tag=@tag + * 2、@tag不存在 + * 1)、存在别名 + * key=对象: tag=key去除别名 + * key=数组: tag=key去除别名 + [] + * 2)、不存在别名 + * tag=key + * tag=key + [] + * @param request + * @param key + * @return + */ + private String buildTag(JSONObject request, String key) { + String _tag = null; + if (request.get(key) instanceof JSONObject && request.getJSONObject(key).getString("@tag") != null) { + _tag = request.getJSONObject(key).getString("@tag"); + } else { + int keyIndex = key.indexOf(":"); + if (keyIndex != -1) { + _tag = key.substring(0, keyIndex); + if (apijson.JSONObject.isTableArray(key)) { + _tag += apijson.JSONObject.KEY_ARRAY; + } + } else { + // 不存在别名 + _tag = key; + } + } + return _tag; + } + + protected JSONObject objectVerify(RequestMethod method, String tag, int version, String name, @NotNull JSONObject request, int maxUpdateCount, SQLCreator creator, JSONObject object) throws Exception { + // 获取指定的JSON结构 >>>>>>>>>>>>>> + JSONObject target = wrapRequest(method, tag, object, true); + // JSONObject clone 浅拷贝没用,Structure.parse 会导致 structure 里面被清空,第二次从缓存里取到的就是 {} + return getVerifier().verifyRequest(method, name, target, request, maxUpdateCount, getGlobalDatabase(), getGlobalSchema(), creator); + } } diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java index 6e10ce2ca..21cb36071 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -761,6 +761,9 @@ public abstract class AbstractSQLConfig implements SQLConfig { } + // mysql8版本以上,子查询支持with as表达式 + private List withAsExpreSqlList = null; + protected List withAsExprePreparedValueList = new ArrayList<>(); private int[] dbVersionNums = null; @Override public int[] getDBVersionNums() { @@ -3909,6 +3912,39 @@ else if (isPresto() || isTrino()) { //key@:{} Subquery <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + /** + * 只要 method != RequestMethod.POST 就都支持 with-as表达式 + * @param cfg + * @param subquery + * @return + * @throws Exception + */ + private String withAsExpreSubqueryString(SQLConfig cfg, Subquery subquery) throws Exception { + if(cfg.getMethod() != RequestMethod.POST && this.withAsExpreSqlList == null) { + this.setWithAsExpreList(); + } + String withAsExpreSql; + if(this.withAsExpreSqlList != null) { + String withQuoteName = getQuote() + subquery.getKey() + getQuote(); + this.withAsExpreSqlList.add(" " + withQuoteName + " AS " + "(" + cfg.getSQL(isPrepared()) + ") "); + withAsExpreSql = " SELECT * FROM " + withQuoteName; + + // 预编译参数 + List subPvl = cfg.getPreparedValueList(); + if (subPvl != null && subPvl.isEmpty() == false) { + this.withAsExprePreparedValueList.addAll(subPvl); + cfg.setPreparedValueList(new ArrayList<>()); + } + }else { + withAsExpreSql = cfg.getSQL(isPrepared()); + // mysql 才存在这个问题, 主表和子表是一张表 + if(this.isMySQL() && StringUtil.equals(this.getTable(), subquery.getFrom())) { + withAsExpreSql = " SELECT * FROM (" + withAsExpreSql+") AS " + getQuote() + subquery.getKey() + getQuote(); + } + } + return withAsExpreSql; + } + @Override public String getSubqueryString(Subquery subquery) throws Exception { if (subquery == null) { @@ -3919,7 +3955,8 @@ public String getSubqueryString(Subquery subquery) throws Exception { SQLConfig cfg = subquery.getConfig(); cfg.setPreparedValueList(new ArrayList<>()); - String sql = (range == null || range.isEmpty() ? "" : range) + "(" + cfg.getSQL(isPrepared()) + ") "; + String withAsExpreSql = withAsExpreSubqueryString(cfg, subquery); + String sql = (range == null || range.isEmpty() ? "" : range) + "(" + withAsExpreSql + ") "; //// SELECT .. FROM(SELECT ..) .. WHERE .. 格式需要把子查询中的预编译值提前 //// 如果外查询 SELECT concat(`name`,?) 这种 SELECT 里也有预编译值,那就不能这样简单反向 @@ -4123,6 +4160,7 @@ public static String getSQL(AbstractSQLConfig config) throws Exception { return null; } + String cSql = null; switch (config.getMethod()) { case POST: return "INSERT INTO " + tablePath + config.getColumnString() + " VALUES" + config.getValuesString(); @@ -4130,12 +4168,16 @@ public static String getSQL(AbstractSQLConfig config) throws Exception { if(config.isClickHouse()){ return "ALTER TABLE " + tablePath + " UPDATE" + config.getSetString() + config.getWhereString(true); } - return "UPDATE " + tablePath + config.getSetString() + config.getWhereString(true) + (config.isMySQL() ? config.getLimitString() : ""); + cSql = "UPDATE " + tablePath + config.getSetString() + config.getWhereString(true) + (config.isMySQL() ? config.getLimitString() : ""); + cSql = buildWithAsExpreSql(config, cSql); + return cSql; case DELETE: if(config.isClickHouse()){ return "ALTER TABLE " + tablePath + " DELETE" + config.getWhereString(true); } - return "DELETE FROM " + tablePath + config.getWhereString(true) + (config.isMySQL() ? config.getLimitString() : ""); // PostgreSQL 不允许 LIMIT + cSql = "DELETE FROM " + tablePath + config.getWhereString(true) + (config.isMySQL() ? config.getLimitString() : ""); // PostgreSQL 不允许 LIMIT + cSql = buildWithAsExpreSql(config, cSql); + return cSql; default: String explain = config.isExplain() ? (config.isSQLServer() ? "SET STATISTICS PROFILE ON " : (config.isOracle() || config.isDameng() || config.isKingBase() ? "EXPLAIN PLAN FOR " : "EXPLAIN ")) : ""; if (config.isTest() && RequestMethod.isGetMethod(config.getMethod(), true)) { // FIXME 为啥是 code 而不是 count ? @@ -4156,8 +4198,32 @@ public static String getSQL(AbstractSQLConfig config) throws Exception { return explain + config.getOraclePageSql(sql); } - return explain + "SELECT " + (config.getCache() == JSONRequest.CACHE_RAM ? "SQL_NO_CACHE " : "") + column + " FROM " + getConditionString(tablePath, config) + config.getLimitString(); + cSql = "SELECT " + (config.getCache() == JSONRequest.CACHE_RAM ? "SQL_NO_CACHE " : "") + column + " FROM " + getConditionString(tablePath, config) + config.getLimitString(); + cSql = buildWithAsExpreSql(config, cSql); + return explain + cSql; + } + } + + private static String buildWithAsExpreSql(@NotNull AbstractSQLConfig config, String cSql) throws Exception { + if(config.withAsExpreSqlList != null && config.withAsExpreSqlList.size() > 0) { + String withAsExpreSql = "WITH "; + // 只有一条 + if(config.withAsExpreSqlList.size() == 1) { + withAsExpreSql += config.withAsExpreSqlList.get(0) + "\n" + cSql; + }else { + int lastIndex = config.withAsExpreSqlList.size() - 1; + for (int i = 0; i < config.withAsExpreSqlList.size(); i++) { + if(i == lastIndex) { + withAsExpreSql += config.withAsExpreSqlList.get(i) + "\n" + cSql; + }else { + withAsExpreSql += config.withAsExpreSqlList.get(i) + ",\n"; + } + } + } + cSql = withAsExpreSql; + config.setWithAsExpreList(); } + return cSql; } /**Oracle的分页获取 @@ -5508,4 +5574,20 @@ public void onMissingKey4Combine(String name, JSONObject request, String combine } + private void setWithAsExpreList() { + // mysql8版本以上,子查询支持with as表达式 + if(this.isMySQL() && this.getDBVersionNums()[0] >= 8) { + this.withAsExpreSqlList = new ArrayList<>(); + } + } + + @Override + public List getWithAsExprePreparedValueList() { + return this.withAsExprePreparedValueList; + } + + @Override + public void setWithAsExprePreparedValueList(List list) { + this.withAsExprePreparedValueList = list; + } } diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java index a9b023d3a..9bbd76d71 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLExecutor.java @@ -1101,6 +1101,15 @@ else if (RequestMethod.isGetMethod(config.getMethod(), true)) { } List valueList = config.isPrepared() ? config.getPreparedValueList() : null; + List withAsExprePreparedValueList = config.isPrepared() ? config.getWithAsExprePreparedValueList() : null; + + // 不同数据库, 预编译mysql使用with-as + if (valueList != null && withAsExprePreparedValueList != null && withAsExprePreparedValueList.size() > 0) { + withAsExprePreparedValueList.addAll(valueList); + valueList = withAsExprePreparedValueList; + // 多条POST/PUT/DELETE语句的情况,需要重新初始化 + config.setWithAsExprePreparedValueList(new ArrayList<>()); + } if (valueList != null && valueList.isEmpty() == false) { for (int i = 0; i < valueList.size(); i++) { diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java index 3b4422bae..334143493 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java @@ -81,6 +81,9 @@ public abstract class AbstractVerifier implements Verifier, IdCallback { private static final String TAG = "AbstractVerifier"; + /**为 PUT, DELETE 强制要求必须有 id/id{} 条件 + */ + public static boolean IS_UPDATE_MUST_HAVE_ID_CONDITION = true; /**开启校验请求角色权限 */ public static boolean ENABLE_VERIFY_ROLE = true; @@ -673,7 +676,7 @@ public JSONObject onParseJSONObject(String key, JSONObject tobj, JSONObject robj } } else { if (RequestMethod.isQueryMethod(method) == false) { - verifyId(method.name(), name, key, robj, finalIdKey, maxUpdateCount, true); + verifyId(method.name(), name, key, robj, finalIdKey, maxUpdateCount, IS_UPDATE_MUST_HAVE_ID_CONDITION); String userIdKey = idCallback == null ? null : idCallback.getUserIdKey(db, sh, ds, key); String finalUserIdKey = StringUtil.isEmpty(userIdKey, false) ? apijson.JSONObject.KEY_USER_ID : userIdKey; @@ -724,7 +727,8 @@ private static void verifyId(@NotNull String method, @NotNull String name, @NotN //批量修改或删除 String idInKey = idKey + "{}"; - + // id引用, 格式: "id{}@": "sql" + String idRefInKey = robj.getString(idKey + "{}@"); JSONArray idIn = null; try { idIn = robj.getJSONArray(idInKey); //如果必须传 id{} ,可在Request表中配置NECESSARY @@ -733,9 +737,9 @@ private static void verifyId(@NotNull String method, @NotNull String name, @NotN + " 里面的 " + idInKey + ":value 中value的类型只能是 [Long] !"); } if (idIn == null) { - if (atLeastOne && id == null) { + if (atLeastOne && id == null && idRefInKey == null) { throw new IllegalArgumentException(method + "请求," + name + "/" + key - + " 里面 " + idKey + " 和 " + idInKey + " 至少传其中一个!"); + + " 里面 " + idKey + "," + idInKey + "," + (idKey + "{}@") + " 至少传其中一个!"); } } else { if (idIn.size() > maxUpdateCount) { //不允许一次操作 maxUpdateCount 条以上记录 @@ -922,7 +926,7 @@ public static JSONObject parse(@NotNull final RequestMethod m if (musts != null && musts.length > 0) { for (String s : musts) { - if (real.get(s) == null) { // 可能传null进来,这里还会通过 real.containsKey(s) == false) { + if (real.get(s) == null && real.get(s+"@") == null) { // 可能传null进来,这里还会通过 real.containsKey(s) == false) { throw new IllegalArgumentException(method + "请求," + name + " 里面不能缺少 " + s + " 等[" + must + "]内的任何字段!"); } @@ -1018,7 +1022,10 @@ public static JSONObject parse(@NotNull final RequestMethod m || mustSet.contains(key) || objKeySet.contains(key)) { continue; } - + // 支持id ref: id{}@ + if (key.endsWith("@") && mustSet.contains(key.substring(0, key.length() - 1))) { + continue; + } refuseSet.add(key); } } diff --git a/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java index 83f488b85..1a0f5e97b 100755 --- a/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/SQLConfig.java @@ -307,4 +307,6 @@ default int[] getDBVersionNums() { + List getWithAsExprePreparedValueList(); + void setWithAsExprePreparedValueList(List withAsExprePreparedValueList); }