From 9d85df047aa02e012d76290960a574e5d45467bf Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:16:44 +0200 Subject: [PATCH] Properties of inherited models can be used on different child models --- .../server/deploy/BeanProperty.java | 33 +++++ .../tests/inheritance/TestPropertyAccess.java | 118 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 ebean-test/src/test/java/org/tests/inheritance/TestPropertyAccess.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index def5457d45..674796eb80 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -668,6 +668,33 @@ public void setTenantValue(EntityBean entityBean, Object tenantId) { setValue(entityBean, tenantId); } + /** + * By default, getIntercept and setIntercept will check, if the passed bean is an instance of descriptor type. + *

+ * If the property is not part of the type hierarchy (i.e. is this property from this descriptor) + * an UnsuppoertedOperation is thrown. + *

+ * If inheritance is involved, this method returns false instead of an exception, if the property might exist in + * one of the child beans. This is necessary for getIntercept, as it returns null in this case. + * @return true if property can be + */ + private boolean checkPropertyAccess(EntityBean bean) { + if (bean == null || descriptor.type().isInstance(bean)) { // null = fall through - NPE is catched later. + return true; + } + InheritInfo inheritInfo = descriptor.inheritInfo(); + if (inheritInfo == null || inheritInfo.isRoot() || !inheritInfo.getRoot().getType().isInstance(bean)) { + throw new IllegalArgumentException(propertyIncomatibleMsg(bean)); + } else { + return false; + } + } + + private String propertyIncomatibleMsg(EntityBean bean) { + String beanType = bean == null ? "null" : bean.getClass().getName(); + return "Property " + name + " on [" + descriptor + "] is incompatible with type[" + beanType + "]"; + } + /** * Set the value of the property without interception or * PropertyChangeSupport. @@ -684,6 +711,9 @@ public void setValue(EntityBean bean, Object value) { * Set the value of the property. */ public void setValueIntercept(EntityBean bean, Object value) { + if (!checkPropertyAccess(bean)) { + throw new IllegalArgumentException(propertyIncomatibleMsg(bean)); + } try { setter.setIntercept(bean, value); } catch (Exception ex) { @@ -795,6 +825,9 @@ public Object getValue(EntityBean bean) { } public Object getValueIntercept(EntityBean bean) { + if (!checkPropertyAccess(bean)) { + return null; + } try { return getter.getIntercept(bean); } catch (Exception ex) { diff --git a/ebean-test/src/test/java/org/tests/inheritance/TestPropertyAccess.java b/ebean-test/src/test/java/org/tests/inheritance/TestPropertyAccess.java new file mode 100644 index 0000000000..b710c67c60 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/inheritance/TestPropertyAccess.java @@ -0,0 +1,118 @@ +package org.tests.inheritance; + +import io.ebean.DB; +import io.ebean.plugin.ExpressionPath; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.*; + +import java.sql.Timestamp; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * @author Roland Praml, FOCONIS AG + */ +public class TestPropertyAccess { + + + private ExpressionPath custCretime = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("cretime"); + private ExpressionPath custName = DB.getDefault().pluginApi().beanType(Customer.class).expressionPath("name"); + // Note: Animal.name is only present in "Cat" not in "Dog" + private ExpressionPath animalName = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("name"); + private ExpressionPath productName = DB.getDefault().pluginApi().beanType(Product.class).expressionPath("name"); + private ExpressionPath animalSpecies = DB.getDefault().pluginApi().beanType(Animal.class).expressionPath("species"); + + @Test + void testOnInheritance() { + Cat cat = new Cat(); + cat.setName("Tom"); + DB.save(cat); + + Dog dog = new Dog(); + dog.setRegistrationNumber("FOO"); + DB.save(dog); + + + Animal animal = DB.find(Animal.class, cat.getId()); + assertThat(animalName.pathGet(animal)).isEqualTo("Tom"); + + animal = DB.find(Animal.class, dog.getId()); + assertThat(animalName.pathGet(animal)).isNull(); + + animalName.pathSet(cat, "Jerry"); + assertThat(cat.getName()).isEqualTo("Jerry"); + + assertThatThrownBy(() -> animalName.pathSet(dog, "Jerry")) + .isInstanceOf(IllegalArgumentException.class); + + animalSpecies.pathSet(cat, "Angora"); + animalSpecies.pathSet(dog, "Bulldog"); + + assertThat(cat.getSpecies()).isEqualTo("Angora"); + assertThat(dog.getSpecies()).isEqualTo("Bulldog"); + } + + @Test + void testOnMappedSuperClass() { + Customer cust = new Customer(); + + Timestamp ts = new Timestamp(123); + custCretime.pathSet(cust, ts); + assertThat(custCretime.pathGet(cust)).isEqualTo(ts); + + custName.pathSet(cust, "Roland"); + assertThat(custName.pathGet(cust)).isEqualTo("Roland"); + + } + + @Test + void testOnPlainBean() { + Product product = new Product(); + + productName.pathSet(product, "Roland"); + assertThat(productName.pathGet(product)).isEqualTo("Roland"); + } + + @Test + void testOnContactGroup() { + ContactGroup cg = new ContactGroup(); + + // CHECKEM: Ist it OK to us the "custCretime" on "contactGroup" + assertThatThrownBy(() -> custCretime.pathGet(cg)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testOnCrossUsage() { + Product product = new Product(); + Customer cust = new Customer(); + Cat cat = new Cat(); + Dog dog = new Dog(); + + + assertThatThrownBy(() -> custCretime.pathGet(product)).isInstanceOf(IllegalArgumentException.class); + custCretime.pathGet(cust); + assertThatThrownBy(() -> custCretime.pathGet(cat)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> custCretime.pathGet(dog)).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> custName.pathGet(product)).isInstanceOf(IllegalArgumentException.class); + custName.pathGet(cust); + assertThatThrownBy(() -> custName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> custName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> animalName.pathGet(product)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> animalName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class); + animalName.pathGet(cat); + animalName.pathGet(dog); + + productName.pathGet(product); + assertThatThrownBy(() -> productName.pathGet(cust)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> productName.pathGet(cat)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> productName.pathGet(dog)).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> animalSpecies.pathGet(product)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> animalSpecies.pathGet(cust)).isInstanceOf(IllegalArgumentException.class); + animalSpecies.pathGet(cat); + animalSpecies.pathGet(dog); + } +}