diff --git a/APIJSONORM/pom.xml b/APIJSONORM/pom.xml index 7a35eb01..692080bf 100644 --- a/APIJSONORM/pom.xml +++ b/APIJSONORM/pom.xml @@ -21,6 +21,12 @@ + + junit + junit + 4.13.2 + test + diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java index defccc93..26e63a9c 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractParser.java @@ -2303,6 +2303,86 @@ protected M getRequestStructure(RequestMethod method, String tag, int version) t KEY_METHOD_ENUM_MAP.put(KEY_DELETE, RequestMethod.DELETE); } + private void parseMethodDirective(String key, RequestMethod keyMethod, @NotNull M request) throws Exception { + boolean isPost = KEY_POST.equals(key); + Object val = request.get(key); + Map obj = val instanceof Map ? JSON.get(request, key) : null; + if (obj == null) { + if (val instanceof String) { + String[] tbls = StringUtil.split((String) val); + if (tbls != null && tbls.length > 0) { + obj = new LinkedHashMap(); + for (String tbl : tbls) { + if (obj.containsKey(tbl)) { + throw new ConflictException(key + ": value 中 " + tbl + " 已经存在,不能重复!"); + } + + obj.put(tbl, isPost && isTableArray(tbl) + ? tbl.substring(0, tbl.length() - 2) + ":[]" : ""); + } + } + } + else { + throw new IllegalArgumentException(key + ": value 中 value 类型错误,只能是 String 或 Map {} !"); + } + } + + Set> set = obj == null ? new HashSet<>() : obj.entrySet(); + for (Entry objEntry : set) { + String objKey = objEntry == null ? null : objEntry.getKey(); + if (objKey == null) { + continue; + } + + Map objAttrMap = new HashMap<>(); + objAttrMap.put(KEY_METHOD, keyMethod); + keyObjectAttributesMap.put(objKey, objAttrMap); + + Object objVal = objEntry.getValue(); + Map objAttrJson = objVal instanceof Map ? JSON.getMap(obj, objKey) : null; + if (objAttrJson == null) { + if (objVal instanceof String) { + objAttrMap.put(KEY_TAG, "".equals(objVal) ? objKey : objVal); + } + else { + throw new IllegalArgumentException(key + ": { " + objKey + ": value 中 value 类型错误,只能是 String 或 Map {} !"); + } + } + else { + boolean hasTag = false; + for (Entry entry : objAttrJson.entrySet()) { + String objAttrKey = entry == null ? null : entry.getKey(); + if (objAttrKey == null) { + continue; + } + + switch (objAttrKey) { + case KEY_DATABASE: + case KEY_DATASOURCE: + case KEY_NAMESPACE: + case KEY_CATALOG: + case KEY_SCHEMA: + case KEY_VERSION: + case KEY_ROLE: + objAttrMap.put(objAttrKey, entry.getValue()); + break; + case KEY_TAG: + hasTag = true; + objAttrMap.put(objAttrKey, entry.getValue()); + break; + default: + break; + } + } + + if (hasTag == false) { + objAttrMap.put(KEY_TAG, isPost && isTableArray(objKey) + ? objKey.substring(0, objKey.length() - 2) + ":[]" : objKey); + } + } + } + } + protected M batchVerify(RequestMethod method, String tag, int version, String name, @NotNull M request, int maxUpdateCount, SQLCreator creator) throws Exception { M correctRequest = JSON.createJSONObject(); List removeTmpKeys = new ArrayList<>(); // 请求json里面的临时变量,不需要带入后面的业务中,比如 @post、@get等 @@ -2312,103 +2392,34 @@ protected M batchVerify(RequestMethod method, String tag, int version, String na throw new IllegalArgumentException("JSON 对象格式不正确 !正确示例例如 \"User\": {}"); } + // 先收集所有显式方法,避免同一请求中方法指令的字段顺序影响对象解析结果。 for (String key : reqSet) { - // key 重复直接抛错(xxx:alias, xxx:alias[]) - if (correctRequest.containsKey(key) || correctRequest.containsKey(key + KEY_ARRAY)) { - throw new IllegalArgumentException("对象名重复,请添加别名区分 ! 重复对象名为: " + key); + RequestMethod keyMethod = KEY_POST.equals(key) ? RequestMethod.POST : KEY_METHOD_ENUM_MAP.get(key); + if (keyMethod == null) { + continue; } - boolean isPost = KEY_POST.equals(key); - // @post、@get 等 RequestMethod + removeTmpKeys.add(key); try { - RequestMethod keyMethod = isPost ? RequestMethod.POST : KEY_METHOD_ENUM_MAP.get(key); - if (keyMethod != null) { - // 如果不匹配,异常不处理即可 - removeTmpKeys.add(key); - - Object val = request.get(key); - Map obj = val instanceof Map ? JSON.get(request, key) : null; - if (obj == null) { - if (val instanceof String) { - String[] tbls = StringUtil.split((String) val); - if (tbls != null && tbls.length > 0) { - obj = new LinkedHashMap(); - for (int i = 0; i < tbls.length; i++) { - String tbl = tbls[i]; - if (obj.containsKey(tbl)) { - throw new ConflictException(key + ": value 中 " + tbl + " 已经存在,不能重复!"); - } - - obj.put(tbl, isPost && isTableArray(tbl) - ? tbl.substring(0, tbl.length() - 2) + ":[]" : ""); - } - } - } - else { - throw new IllegalArgumentException(key + ": value 中 value 类型错误,只能是 String 或 Map {} !"); - } - } - - Set> set = obj == null ? new HashSet<>() : obj.entrySet(); - - for (Entry objEntry : set) { - String objKey = objEntry == null ? null : objEntry.getKey(); - if (objKey == null) { - continue; - } - - Map objAttrMap = new HashMap<>(); - objAttrMap.put(KEY_METHOD, keyMethod); - keyObjectAttributesMap.put(objKey, objAttrMap); - - Object objVal = objEntry.getValue(); - Map objAttrJson = objVal instanceof Map ? JSON.getMap(obj, objKey) : null; - if (objAttrJson == null) { - if (objVal instanceof String) { - objAttrMap.put(KEY_TAG, "".equals(objVal) ? objKey : objVal); - } - else { - throw new IllegalArgumentException(key + ": { " + objKey + ": value 中 value 类型错误,只能是 String 或 Map {} !"); - } - } - else { - Set> objSet = objAttrJson.entrySet(); - - boolean hasTag = false; - for (Entry entry : objSet) { - String objAttrKey = entry == null ? null : entry.getKey(); - if (objAttrKey == null) { - continue; - } + parseMethodDirective(key, keyMethod, request); + } + catch (Exception e) { + Log.e(TAG, "parse method directive failed", e); + throw e; + } + } - switch (objAttrKey) { - case KEY_DATABASE: - case KEY_DATASOURCE: - case KEY_NAMESPACE: - case KEY_CATALOG: - case KEY_SCHEMA: - case KEY_VERSION: - case KEY_ROLE: - objAttrMap.put(objAttrKey, entry.getValue()); - break; - case KEY_TAG: - hasTag = true; - objAttrMap.put(objAttrKey, entry.getValue()); - break; - default: - break; - } - } + for (String key : reqSet) { + // key 重复直接抛错(xxx:alias, xxx:alias[]) + if (correctRequest.containsKey(key) || correctRequest.containsKey(key + KEY_ARRAY)) { + throw new IllegalArgumentException("对象名重复,请添加别名区分 ! 重复对象名为: " + key); + } - if (hasTag == false) { - objAttrMap.put(KEY_TAG, isPost && isTableArray(objKey) - ? objKey.substring(0, objKey.length() - 2) + ":[]" : objKey); - } - } - } - continue; - } + if (KEY_POST.equals(key) || KEY_METHOD_ENUM_MAP.containsKey(key)) { + continue; + } + try { // 1、非crud,对于没有显式声明操作方法的,直接用 URL(/get, /post 等) 对应的默认操作方法 // 2、crud, 没有声明就用 GET // 3、兼容 sql@ Map,设置 GET方法 diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java new file mode 100644 index 00000000..dc96bfb0 --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractParserMethodDirectiveTest.java @@ -0,0 +1,193 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractParserMethodDirectiveTest { + private static JSONParser, ? extends List> previousJSONParser; + + @BeforeClass + public static void setUpJSONParser() { + previousJSONParser = JSON.DEFAULT_JSON_PARSER; + JSON.DEFAULT_JSON_PARSER = new JSONParser, List>() { + @Override + public Map createJSONObject() { + return new LinkedHashMap<>(); + } + + @Override + public List createJSONArray() { + return new ArrayList<>(); + } + + @Override + public Object parse(Object json) { + return json; + } + + @Override + @SuppressWarnings("unchecked") + public Map parseObject(Object json) { + return (Map) json; + } + + @Override + public T parseObject(Object json, Class clazz) { + return clazz.cast(json); + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json) { + return (List) json; + } + + @Override + @SuppressWarnings("unchecked") + public List parseArray(Object json, Class clazz) { + return (List) json; + } + + @Override + public String toJSONString(Object obj, boolean format) { + return String.valueOf(obj); + } + }; + } + + @AfterClass + public static void restoreJSONParser() { + JSON.DEFAULT_JSON_PARSER = previousJSONParser; + } + + @Test + public void dispatchesMethodDirectivesBeforeBusinessObjectsRegardlessOfOrder() throws Exception { + TestParser parser = new TestParser(); + Map request = new LinkedHashMap<>(); + + Map delete = new LinkedHashMap<>(); + delete.put("id", 1L); + request.put("Moment", delete); + request.put("@delete", "Moment"); + + List comments = new ArrayList<>(); + comments.add(new LinkedHashMap()); + request.put("Comment:new[]", comments); + request.put("@post", "Comment:new[]"); + + parser.run(request); + + assertSame(RequestMethod.DELETE, parser.methods.get("Moment")); + assertSame(RequestMethod.POST, parser.methods.get("Comment:new[]")); + } + + @Test + public void keepsObjectMethodPriorityOverGlobalDirective() throws Exception { + TestParser parser = new TestParser(); + Map request = new LinkedHashMap<>(); + Map moment = new LinkedHashMap<>(); + moment.put("@method", RequestMethod.PUT.name()); + moment.put("id", 1L); + request.put("Moment", moment); + request.put("@delete", "Moment"); + + parser.run(request); + + assertSame(RequestMethod.PUT, parser.methods.get("Moment")); + } + + @Test + public void rejectsArrayMethodDirectiveValues() throws Exception { + Map request = new LinkedHashMap<>(); + List directive = new ArrayList<>(); + directive.add("Moment"); + request.put("@delete", directive); + request.put("Moment", new LinkedHashMap()); + + try { + new TestParser().run(request); + fail("List-valued method directive should be rejected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("String")); + assertTrue(e.getMessage().contains("Map")); + } + } + + private static final class TestParser extends AbstractParser, List> { + private final Map methods = new LinkedHashMap<>(); + + private TestParser() { + super(RequestMethod.CRUD, false); + } + + private Map run(Map request) throws Exception { + return batchVerify(RequestMethod.CRUD, null, 0, null, request, 10, this); + } + + @Override + protected Map getRequestStructure(RequestMethod method, String tag, int version) { + return new LinkedHashMap<>(); + } + + @Override + protected Map objectVerify(RequestMethod method, String tag, int version, String name, + Map request, int maxUpdateCount, + SQLCreator, List> creator, Map object) { + methods.put(request.keySet().iterator().next(), method); + return request; + } + + @Override + public Object onFunctionParse(String key, String function, String parentPath, String currentName, + Map currentObject, boolean containRaw) { + return null; + } + + @Override + public ObjectParser, List> createObjectParser( + Map request, String parentPath, + SQLConfig, List> arrayConfig, + boolean isSubquery, boolean isTable, boolean isArrayMainTable) { + return null; + } + + @Override + public Parser, List> createParser() { + return new TestParser(); + } + + @Override + public FunctionParser, List> createFunctionParser() { + return null; + } + + @Override + public Verifier, List> createVerifier() { + return null; + } + + @Override + public SQLConfig, List> createSQLConfig() { + return null; + } + + @Override + public SQLExecutor, List> createSQLExecutor() { + return null; + } + } +} diff --git a/README-extend.md b/README-extend.md index 98b60ac2..022a84a0 100644 --- a/README-extend.md +++ b/README-extend.md @@ -52,6 +52,29 @@ key= Moment[] } } +同一事务中对同一张表执行删除和批量新增时,需要通过别名保证 key 唯一。`@post`、`@delete` 等方法指令的 value 只能是 String 或 JSONObject,不能使用 JSONArray: + +```json +{ + "@delete": { + "MobilizeTaskInfo:remove": "MobilizeTaskInfo" + }, + "MobilizeTaskInfo:remove": { + "id{}": ["09a7a740-1935-469a-ab76-ea1ee1f94ddc"] + }, + "@post": { + "MobilizeTaskInfo:add[]": "MobilizeTaskInfo:[]" + }, + "MobilizeTaskInfo:add[]": [ + { + "leading_unit_name": "市交通局", + "serial_number": "GD2025-064", + "status": 1 + } + ] +} +``` + 对于没有显式声明操作方法的,直接用 URL(/get, /post 等) 对应的默认操作方法 ```