From cb44b390ad7f962bf34d3a740a82c855c9fe0f6f Mon Sep 17 00:00:00 2001 From: Henrique Date: Sun, 30 Aug 2026 08:55:56 -0300 Subject: [PATCH 1/2] fix: fail fast on invalid fake-delete config Initialize the fake-delete access map eagerly to prevent null dereferences. Reject missing or invalid per-table deletedKey values so fake delete cannot silently fall back to physical deletion. issue #654 --- .../java/apijson/orm/AbstractSQLConfig.java | 39 ++-- .../java/apijson/orm/AbstractVerifier.java | 2 + .../orm/AbstractSQLConfigFakeDeleteTest.java | 214 ++++++++++++++++++ 3 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java index fca28432..3c7bb4d4 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -6195,25 +6195,30 @@ else if (w.startsWith("!")) { if (enableFakeDelete && method == DELETE) { // 查询 Access 假删除 - Map accessFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP.get(config.getTable()); - - Object deletedKey = accessFakeDeleteMap.get(KEY_DELETED_KEY); - if (StringUtil.isNotEmpty(deletedKey, true)) { - // 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的 - Map fakeDeleteMap = new HashMap<>(); - fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE)); - fakeDeleteMap = config.onFakeDelete(fakeDeleteMap); - - Map content = config.getContent(); - if (content == null || content.isEmpty()) { - content = fakeDeleteMap; - } else { - content.putAll(fakeDeleteMap); - } + Map> fakeDeleteConfigMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + Map accessFakeDeleteMap = fakeDeleteConfigMap == null + ? null : fakeDeleteConfigMap.get(config.getTable()); - config.setMethod(PUT); - config.setContent(content); + Object deletedKey = accessFakeDeleteMap == null ? null : accessFakeDeleteMap.get(KEY_DELETED_KEY); + if ((deletedKey instanceof String) == false || StringUtil.isNotEmpty(deletedKey, true) == false) { + throw new IllegalArgumentException(config.getTable() + " 对应的假删除配置错误!" + + KEY_DELETED_KEY + ":value 中 value 必须为非空 String!当前值为 " + deletedKey); } + + // 假删除需要更新的其他字段,比如:删除时间 deletedTime 之类的 + Map fakeDeleteMap = new HashMap<>(); + fakeDeleteMap.put(deletedKey.toString(), accessFakeDeleteMap.get(KEY_DELETED_VALUE)); + fakeDeleteMap = config.onFakeDelete(fakeDeleteMap); + + Map content = config.getContent(); + if (content == null || content.isEmpty()) { + content = fakeDeleteMap; + } else { + content.putAll(fakeDeleteMap); + } + + config.setMethod(PUT); + config.setContent(content); } List cs = new ArrayList<>(); diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java index 6a59e0ef..90b84186 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractVerifier.java @@ -177,6 +177,8 @@ public abstract class AbstractVerifier, L exten ACCESS_MAP = new HashMap<>(SYSTEM_ACCESS_MAP); + ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>(); + REQUEST_MAP = new HashMap<>(ACCESS_MAP.size()*7); // 单个与批量增删改 COMPILE_MAP = new HashMap(); diff --git a/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java b/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java new file mode 100644 index 00000000..cd31e28b --- /dev/null +++ b/APIJSONORM/src/test/java/apijson/orm/AbstractSQLConfigFakeDeleteTest.java @@ -0,0 +1,214 @@ +package apijson.orm; + +import apijson.JSON; +import apijson.JSONParser; +import apijson.RequestMethod; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +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.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class AbstractSQLConfigFakeDeleteTest { + private static final String TABLE = "User"; + private static final Map> EAGERLY_INITIALIZED_MAP = + AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + + private static JSONParser, ? extends List> previousJSONParser; + private Map> previousFakeDeleteMap; + + @BeforeClass + public static void installJSONParser() { + 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; + } + + @Before + public void resetFakeDeleteMap() { + previousFakeDeleteMap = AbstractVerifier.ACCESS_FAKE_DELETE_MAP; + AbstractVerifier.ACCESS_FAKE_DELETE_MAP = new LinkedHashMap<>(); + } + + @After + public void restoreFakeDeleteMap() { + AbstractVerifier.ACCESS_FAKE_DELETE_MAP = previousFakeDeleteMap; + } + + @Test + public void eagerlyInitializesFakeDeleteMap() { + assertNotNull(EAGERLY_INITIALIZED_MAP); + } + + @Test + public void keepsGetWhenTableHasNoFakeDeleteConfig() throws Exception { + SQLConfig, List> config = newConfig(RequestMethod.GET, true); + + assertEquals(RequestMethod.GET, config.getMethod()); + } + + @Test + public void rejectsDeleteWhenTableConfigIsMissing() throws Exception { + assertInvalidConfig(null); + } + + @Test + public void rejectsDeleteWhenTableConfigIsEmpty() throws Exception { + assertInvalidConfig(new LinkedHashMap()); + } + + @Test + public void rejectsDeleteWhenDeletedKeyIsBlank() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, " "); + + assertInvalidConfig(tableConfig); + } + + @Test + public void rejectsDeleteWhenDeletedKeyIsNotAString() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, 123); + + assertInvalidConfig(tableConfig); + } + + @Test + public void rewritesConfiguredDeleteAsPut() throws Exception { + Map tableConfig = new LinkedHashMap<>(); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_KEY, "deletedFlag"); + tableConfig.put(AbstractSQLConfig.KEY_DELETED_VALUE, 1); + AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig); + + SQLConfig, List> config = newConfig(RequestMethod.DELETE, true); + + assertEquals(RequestMethod.PUT, config.getMethod()); + assertNotNull(config.getContent()); + assertEquals(Integer.valueOf(1), config.getContent().get("deletedFlag")); + } + + @Test + public void keepsDeleteWhenFakeDeleteIsDisabled() throws Exception { + SQLConfig, List> config = newConfig(RequestMethod.DELETE, false); + + assertEquals(RequestMethod.DELETE, config.getMethod()); + } + + private static void assertInvalidConfig(Map tableConfig) throws Exception { + if (tableConfig != null) { + AbstractVerifier.ACCESS_FAKE_DELETE_MAP.put(TABLE, tableConfig); + } + + try { + SQLConfig, List> config = + newConfig(RequestMethod.DELETE, true); + fail("Invalid fake-delete config must throw instead of returning " + config.getMethod()); + } + catch (IllegalArgumentException e) { + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().contains(TABLE)); + assertTrue(e.getMessage().contains(AbstractSQLConfig.KEY_DELETED_KEY)); + assertTrue(e.getMessage().contains("String")); + } + } + + private static SQLConfig, List> newConfig( + RequestMethod method, final boolean fakeDelete) throws Exception { + Map request = new LinkedHashMap<>(); + request.put("id", 1L); + + AbstractSQLConfig.Callback, List> callback = + new AbstractSQLConfig.SimpleCallback, List>() { + @Override + public SQLConfig, List> getSQLConfig(RequestMethod method, + String database, String datasource, String namespace, String catalog, String schema, + String table) { + return new AbstractSQLConfig, List>(method, table) { + @Override + public boolean isFakeDelete() { + return fakeDelete; + } + + @Override + public String gainDBVersion() { + return "8.0.0"; + } + + @Override + public String gainDBUri() { + return "jdbc:mysql://localhost/test"; + } + + @Override + public String gainDBAccount() { + return "test"; + } + + @Override + public String gainDBPassword() { + return "test"; + } + }; + } + }; + + return AbstractSQLConfig.newSQLConfig(method, TABLE, null, request, null, false, callback); + } +} From df8529e9ca7f5ea675b210c8660b29b5d1efcc92 Mon Sep 17 00:00:00 2001 From: TommyLemon <1184482681@qq.com> Date: Sun, 30 Aug 2026 22:41:09 +0800 Subject: [PATCH 2/2] Update AbstractSQLConfig.java --- APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java index 3c7bb4d4..028c7db7 100755 --- a/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java +++ b/APIJSONORM/src/main/java/apijson/orm/AbstractSQLConfig.java @@ -6200,7 +6200,7 @@ else if (w.startsWith("!")) { ? null : fakeDeleteConfigMap.get(config.getTable()); Object deletedKey = accessFakeDeleteMap == null ? null : accessFakeDeleteMap.get(KEY_DELETED_KEY); - if ((deletedKey instanceof String) == false || StringUtil.isNotEmpty(deletedKey, true) == false) { + if ((deletedKey instanceof String) == false || StringUtil.isEmpty(deletedKey, true)) { throw new IllegalArgumentException(config.getTable() + " 对应的假删除配置错误!" + KEY_DELETED_KEY + ":value 中 value 必须为非空 String!当前值为 " + deletedKey); }