diff --git a/common/src/main/java/com/highgo/cloud/util/BeanUtil.java b/common/src/main/java/com/highgo/cloud/util/BeanUtil.java index e128dab..6c0abeb 100644 --- a/common/src/main/java/com/highgo/cloud/util/BeanUtil.java +++ b/common/src/main/java/com/highgo/cloud/util/BeanUtil.java @@ -401,6 +401,9 @@ public static Map toMap(Object bean) { String propertyName = descriptor.getName(); if (!propertyName.equals("class")) { Method readMethod = descriptor.getReadMethod(); + if (readMethod == null) { + continue; + } Object result = readMethod.invoke(bean, new Object[0]); returnMap.put(propertyName, result); } diff --git a/tests/regression/BeanReadablePropertiesRegression.java b/tests/regression/BeanReadablePropertiesRegression.java new file mode 100644 index 0000000..4b858cf --- /dev/null +++ b/tests/regression/BeanReadablePropertiesRegression.java @@ -0,0 +1,116 @@ +import com.highgo.cloud.util.BeanUtil; +import java.util.HashMap; +import java.util.Map; + +/** Full real BeanUtil entry points; fixtures are ordinary JavaBeans, not framework doubles. */ +public final class BeanReadablePropertiesRegression { + private static int failures; + + public static final class WriteOnlyBean { + private String name; + private String note; + private String secret; + + public String getName() { return name; } + public void setName(String value) { name = value; } + public String getNote() { return note; } + public void setNote(String value) { note = value; } + public void setSecret(String value) { secret = value; } + public String peekSecret() { return secret; } + } + + public static final class PlainBean { + private String name; + private String note; + + public String getName() { return name; } + public void setName(String value) { name = value; } + public String getNote() { return note; } + public void setNote(String value) { note = value; } + public String getReadOnly() { return "readable"; } + } + + private static void require(boolean condition, String message) { + if (!condition) { throw new AssertionError(message); } + } + + private static void run(String name, Runnable test) { + try { + test.run(); + System.out.println("PASS " + name); + } catch (Throwable error) { + failures++; + System.err.println("FAIL " + name + ": " + error); + error.printStackTrace(System.err); + } + } + + public static void main(String[] args) { + run("toMap ignores a setter-only property", () -> { + WriteOnlyBean bean = new WriteOnlyBean(); + bean.setName("alice"); + bean.setSecret("private"); + Map map = BeanUtil.toMap(bean); + require("alice".equals(map.get("name")), "readable name lost"); + require(map.containsKey("note") && map.get("note") == null, "null getter value lost"); + require(!map.containsKey("secret") && !map.containsKey("class"), "unreadable or class property exposed"); + require(map.size() == 2, "unexpected property set: " + map); + }); + run("bean-to-map copy keeps readable fields and ignore list", () -> { + WriteOnlyBean bean = new WriteOnlyBean(); + bean.setName("alice"); + bean.setNote("ignored"); + Map map = new HashMap<>(); + map.put("existing", 7); + BeanUtil.copyProperties(bean, map, "note"); + require("alice".equals(map.get("name")), "readable field not copied"); + require(Integer.valueOf(7).equals(map.get("existing")), "existing entry changed"); + require(!map.containsKey("note") && !map.containsKey("secret"), "excluded field copied"); + }); + run("copyNotNullProperties reaches real assignment with setter-only beans", () -> { + WriteOnlyBean source = new WriteOnlyBean(); + source.setName("updated"); + source.setSecret("source secret"); + WriteOnlyBean target = new WriteOnlyBean(); + target.setName("old"); + target.setNote("retained"); + target.setSecret("target secret"); + BeanUtil.copyNotNullProperties(source, target); + require("updated".equals(target.getName()), "non-null value not copied"); + require("retained".equals(target.getNote()), "existing null-copy policy changed"); + require("target secret".equals(target.peekSecret()), "unreadable value was copied"); + }); + run("ordinary readable bean and read-only getter control", () -> { + PlainBean bean = new PlainBean(); + bean.setName("plain"); + Map map = BeanUtil.toMap(bean); + require("plain".equals(map.get("name")), "ordinary value changed"); + require("readable".equals(map.get("readOnly")), "read-only getter excluded"); + require(map.containsKey("note") && map.get("note") == null, "null value excluded"); + require(!map.containsKey("class") && map.size() == 3, "class filtering changed"); + }); + run("ordinary bean-to-map ignore control", () -> { + PlainBean bean = new PlainBean(); + bean.setName("plain"); + bean.setNote("hidden"); + Map map = new HashMap<>(); + BeanUtil.copyProperties(bean, map, "note"); + require("plain".equals(map.get("name")) && !map.containsKey("note"), "ignore list changed"); + require("readable".equals(map.get("readOnly")), "read-only getter not copied"); + }); + run("throwing getter remains a real PropertyException control", () -> { + try { + BeanUtil.toMap(new ThrowingBean()); + throw new AssertionError("getter exception was hidden"); + } catch (com.highgo.cloud.exception.PropertyException expected) { + require(expected.getCause() != null, "getter exception cause lost"); + } + }); + if (failures != 0) { throw new AssertionError(failures + " cases failed"); } + System.out.println("6 cases passed"); + } + + public static final class ThrowingBean { + public String getValue() { throw new IllegalStateException("getter failure"); } + } +}