BeanCopyUtil.copyProperties copies every numeric field but silently skips every boolean and char one, so SimpleJsonDataOperator.update(T) loses a boolean change when the caller passes a detached entity.
Measured
utils/BeanCopyUtil.java reads each source field with Field.get, which boxes. For a primitive target field it then tests targetField.getType().isAssignableFrom(value.getClass()) — boolean.class.isAssignableFrom(Boolean.class) is false — and falls through to convertValue, whose branches are: same type, String, and value instanceof Number. Boolean is none of the three, so convertValue returns null and copyFieldValue returns without writing. convertNumber does handle int/long/double/float/short/byte, which is why only boolean and char are affected.
Probe against the real operator (UltiEssentials BanData, whose active is a primitive boolean, JSON backend over a temp directory, framework 6.3.0-SNAPSHOT):
PROBE copy id=747904b4-… active=true
copy.setActive(false); store.update(copy);
PROBE after active=true <-- the change was dropped
SimpleJsonDataOperator.update(T) is the reachable consumer: it resolves the cached instance for the incoming entity's id and calls BeanCopyUtil.copyProperties(obj, old, "id"). When a caller passes the cached instance itself, the copy is a self-copy and the mutation is already present, so nothing is lost — that is why no module has noticed. When a caller passes a detached instance with the same id (a copy, a Gson round trip, an entity rebuilt from a panel payload), every boolean field silently keeps its old value.
Suggested fix
Give convertValue a Boolean/boolean branch (and a Character/char one), or compare against the boxed equivalent of a primitive target type before deciding a conversion is needed. A test over an entity with one field of each primitive type would pin the whole set at once.
Scope
Found while writing the UltiEssentials wave-1 test fixtures for UltiKits/UltiEssentials#35 (Phase 17). Not itself a defect any current module triggers — every module passes the cached instance — so filing rather than fixing, per this phase's new-finding routing.
BeanCopyUtil.copyPropertiescopies every numeric field but silently skips everybooleanandcharone, soSimpleJsonDataOperator.update(T)loses a boolean change when the caller passes a detached entity.Measured
utils/BeanCopyUtil.javareads each source field withField.get, which boxes. For a primitive target field it then teststargetField.getType().isAssignableFrom(value.getClass())—boolean.class.isAssignableFrom(Boolean.class)isfalse— and falls through toconvertValue, whose branches are: same type,String, andvalue instanceof Number.Booleanis none of the three, soconvertValuereturnsnullandcopyFieldValuereturns without writing.convertNumberdoes handleint/long/double/float/short/byte, which is why onlybooleanandcharare affected.Probe against the real operator (UltiEssentials
BanData, whoseactiveis a primitiveboolean, JSON backend over a temp directory, framework6.3.0-SNAPSHOT):SimpleJsonDataOperator.update(T)is the reachable consumer: it resolves the cached instance for the incoming entity's id and callsBeanCopyUtil.copyProperties(obj, old, "id"). When a caller passes the cached instance itself, the copy is a self-copy and the mutation is already present, so nothing is lost — that is why no module has noticed. When a caller passes a detached instance with the same id (a copy, a Gson round trip, an entity rebuilt from a panel payload), every boolean field silently keeps its old value.Suggested fix
Give
convertValueaBoolean/booleanbranch (and aCharacter/charone), or compare against the boxed equivalent of a primitive target type before deciding a conversion is needed. A test over an entity with one field of each primitive type would pin the whole set at once.Scope
Found while writing the UltiEssentials wave-1 test fixtures for UltiKits/UltiEssentials#35 (Phase 17). Not itself a defect any current module triggers — every module passes the cached instance — so filing rather than fixing, per this phase's new-finding routing.