diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Bar.java b/core/src/test/java/org/incenp/linkml/core/playground/Bar.java new file mode 100644 index 0000000..058010c --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Bar.java @@ -0,0 +1,19 @@ +package org.incenp.linkml.core.playground; + +/** + * An example of a class that is used in a “refined” slot. + *

+ * This class is used in the {@link Foo} class. Some of the classes that are + * derived from Foo uses derived classes instead. + */ +public class Bar { + private String name; + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java new file mode 100644 index 0000000..e047920 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedBar.java @@ -0,0 +1,20 @@ +package org.incenp.linkml.core.playground; + +/** + * First derived class from Bar. + *

+ * This class is used, instead of its parent Bar in + * {@link FirstDerivedFoo}. + */ +public class FirstDerivedBar extends Bar { + + private int length; + + public int getLength() { + return length; + } + + public void setLength(int value) { + length = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java new file mode 100644 index 0000000..628ddea --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/FirstDerivedFoo.java @@ -0,0 +1,125 @@ +package org.incenp.linkml.core.playground; + +import java.util.List; + +/** + * An example of a class that refines the range of its slots to make them accept + * only a more specialised subclass. + */ +public class FirstDerivedFoo extends Foo { + + /* + * Overridden read accessor for the `bar` slot. + * + * We override it to ensure that it returns the more specialised subtype. + */ + @Override + public FirstDerivedBar getBar() { + // This cast is perfectly safe because the write accessor below guarantees that + // only a FirstDerivedBar object can be assigned to the slot. + return (FirstDerivedBar) super.getBar(); + } + + /* + * Overridden write accessor for the `bar` slot. + * + * We override it to add a runtime check to enforce the more specialised type + * constraint. We cannot prevent client code from trying to assign an object of + * the wrong type, but if that happens we can at least immediately throw an + * exception. + */ + @Override + public void setBar(Bar value) { + if ( !(value instanceof FirstDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Overloaded write accessor for the `bar` slot. + * + * This accessor is not strictly necessary, but it makes it clearer that in this + * class, the value of the `bar` slot should be a `FirstDerivedBar`. It also + * allows to bypass the dynamic check in the normal accessor above, if the + * compiler already knows that the assigned value is a FirstDerivedBar. + */ + public void setBar(FirstDerivedBar value) { + super.setBar(value); + } + + /* + * Overridden “Standard” read accessor. + * + * We override it to ensure it returns the more specialised subtype. + * + * Because the slot could be (and instead is, in this example) refined further + * in subclasses, we must still return a generic wildcard, so this accessor has + * the same limitation as the one it overrides in the `Foo` class: modifying the + * returned list requires an explicit cast into a non-wildcard form. + */ + @Override + @SuppressWarnings("unchecked") + public List getBars() { + // This cast should be safe IFF nobody explicitly modify the value returned by + // this accessor after casting it into a `List`. + return (List) super.getBars(); + } + + /* + * Overridden read accessor with optional creation of the list. + * + * We must override this accessor to ensure that the created list (if the list + * needs to be created) is using the more specialised type. + */ + @Override + public List getBars(boolean create) { + // We can delegate the logic to the parent + return super.getBars(FirstDerivedBar.class, create); + } + + /* + * Overridden parameterised read accessor. + * + * We must override this accessor to add a runtime check that the given type + * parameter is compatible with the more specialised type. + */ + @Override + public List getBars(Class t) { + if ( !FirstDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t); + } + + /* + * Overridden parameterised read accessor with optional creation of the list. + * + * Same as above: we must override this accessor to add a runtime check on the + * type parameter. + */ + @Override + public List getBars(Class t, boolean create) { + if ( !FirstDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t, create); + } + + /* + * Overridden “standard” write accessor. + * + * We must override this accessor to include a runtime check. The check must be + * performed on all list items. + */ + @Override + public void setBars(List value) { + for ( Bar b : value ) { + if ( !(b instanceof FirstDerivedBar) ) { + throw new IllegalArgumentException("Invalid bars value"); + } + } + super.setBars(value); + } + +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Foo.java b/core/src/test/java/org/incenp/linkml/core/playground/Foo.java new file mode 100644 index 0000000..5576616 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Foo.java @@ -0,0 +1,100 @@ +package org.incenp.linkml.core.playground; + +import java.util.ArrayList; +import java.util.List; + +/** + * An example of a class whose slots are “refined” in derived classes. + */ +public class Foo { + + private Bar bar; + + // We use a wildcard generic to allow derived classes to “refine” the parameter + // type + private List bars; + + /* + * Accessors for the `bar` slot. + * + * Nothing out of the ordinary here. + */ + public Bar getBar() { + return bar; + } + + public void setBar(Bar value) { + bar = value; + } + + /* + * Accessors for the multi-valued `bars` slot. + */ + + /* + * “Standard” read accessor. Its return type is parameterized with a wildcard + * generic to allow derived classes to refine the parameter. + * + * Modifying the returned list is only possible by explicitly casting it to a + * non-wildcard form, as in: + * + * ((List) foo.getBars()).add(new Bar()); + * + * Without the cast, the following would be a compile-time error: + * + * foo.getBars().add(new Bar()); + */ + public List getBars() { + return bars; + } + + /* + * LinkML-Java “Standard” read accessor with optional creation of the list. + * + * This is a convenience accessor, intended to allow client code to dispense + * with a null-ness check. + * + * As for the argument-less read accessor, the return type is a wildcard, so + * modifying the returned list requires an explicit cast. + */ + public List getBars(boolean create) { + if ( bars == null && create ) { + bars = new ArrayList(); + } + return bars; + } + + /* + * Parameterised read accessor. + * + * This is another convenience accessor. This one is intended to allow client + * code to dispense with an explicit cast to modify the list: + * + * foo.getBars(Bar.class).add(new Bar()); + */ + @SuppressWarnings("unchecked") + public List getBars(Class t) { + return (List) bars; + } + + /* + * Parameterised read accessor with optional creation of the list. + * + * This is another convenience accessor, combining the effects of the two + * accessors above. + */ + @SuppressWarnings("unchecked") + public List getBars(Class t, boolean create) { + if ( bars == null && create ) { + bars = new ArrayList(); + } + return (List) bars; + } + + /* + * “Standard” write accessor. + */ + public void setBars(List value) { + bars = value; + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/Playground.java b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java new file mode 100644 index 0000000..d137b8d --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/Playground.java @@ -0,0 +1,115 @@ +package org.incenp.linkml.core.playground; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.WildcardType; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class Playground { + + @Test + void testFoo() { + Foo f = new Foo(); + f.getBars(Bar.class, true).add(new Bar()); + // Can add a derived Bar + f.getBars(Bar.class).add(new FirstDerivedBar()); + } + + @Test + void testFirstDerivedFoo() { + FirstDerivedFoo fdf = new FirstDerivedFoo(); + try { + fdf.getBars(Bar.class, true).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + fdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + fdf.getBars().get(0).setLength(9); + + Foo f = fdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testSecondDerivedFoo() { + SecondDerivedFoo sdf = new SecondDerivedFoo(); + try { + sdf.getBars(Bar.class, true).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + sdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + sdf.getBars().get(0).setLength(9); + + FirstDerivedFoo fdf = sdf; + try { + // Even when accessed from a FirstDerivedFoo object, cannot assign a Bar + fdf.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + Foo f = sdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testThirdDerivedFoo() { + ThirdDerivedFoo tdf = new ThirdDerivedFoo(); + try { + tdf.getBars(FirstDerivedBar.class, true).add(new FirstDerivedBar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + + tdf.getBars(SecondDerivedBar.class, true).add(new SecondDerivedBar()); + // Can access bars items as FirstDerivedBar objects in read mode + tdf.getBars().get(0).setLength(9); + + Foo f = tdf; + try { + // Even when accessed from a Foo object, cannot assign a Bar + f.getBars(Bar.class).add(new Bar()); + Assertions.fail("Wrong assignment not caught"); + } catch ( IllegalArgumentException iae ) { + Assertions.assertEquals("Invalid type parameter", iae.getMessage()); + } + } + + @Test + void testObtainingParameterBound() { + Class klass = FirstDerivedFoo.class; + Method m = null; + try { + m = klass.getDeclaredMethod("getBars", (Class[]) null); + } catch ( NoSuchMethodException | SecurityException e ) { + Assertions.fail("No getBars method"); + } + + ParameterizedType t = (ParameterizedType) m.getGenericReturnType(); + WildcardType t2 = (WildcardType) t.getActualTypeArguments()[0]; + Assertions.assertEquals(FirstDerivedBar.class, t2.getUpperBounds()[0]); + } +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java new file mode 100644 index 0000000..b13c9a4 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedBar.java @@ -0,0 +1,11 @@ +package org.incenp.linkml.core.playground; + +/** + * Second derived class from Bar. + *

+ * This class is used, instead of its parent FirstDerivedBar, in + * {@link ThirdDerivedFoo}. + */ +public class SecondDerivedBar extends FirstDerivedBar { + +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java new file mode 100644 index 0000000..763a144 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/SecondDerivedFoo.java @@ -0,0 +1,13 @@ +package org.incenp.linkml.core.playground; + +/** + * An example of a class that inherits from a class that refines its slots, but + * that does no do any refinement itself. + */ +public class SecondDerivedFoo extends FirstDerivedFoo { + + /* + * In this class, the `bar` and `bars` slot are of the same type as in the + * parental `FirstDerivedFoo` class, so no overriding of accessors is necessary. + */ +} diff --git a/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java b/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java new file mode 100644 index 0000000..9ab295c --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/playground/ThirdDerivedFoo.java @@ -0,0 +1,139 @@ +package org.incenp.linkml.core.playground; + +import java.util.List; + +/** + * An example of a class that refines the range of its slots, that it inherited + * from a class that already refined them. + * + * Importantly, this class has no derived class, so we know its slots cannot be + * further refined by another class. + */ +public class ThirdDerivedFoo extends SecondDerivedFoo { + + /* + * Overridden read accessor for the `bar` slot. + * + * We override it to ensure that it returns the more specialised subtype. + */ + @Override + public SecondDerivedBar getBar() { + return (SecondDerivedBar) super.getBar(); + } + + /* + * Overridden write accessor for the `bar` slot. + * + * We override it to add a runtime check to enforce the more specialised type + * constraint. We cannot prevent client code from trying to assign an object of + * the wrong type, but if that happens we can at least immediately throw an + * exception. + */ + @Override + public void setBar(Bar value) { + if ( !(value instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Second overridden write accessor for the `bar` slot. + * + * Since `FirstDerivedFoo` defined this accessor, we must override it as well, + * otherwise it would allow client code to assign a FirstDerivedBar to the slot. + */ + @Override + public void setBar(FirstDerivedBar value) { + if ( !(value instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + super.setBar(value); + } + + /* + * Overloaded write accessor for the `bar` slot. + * + * This accessor is not strictly necessary, but it makes it clearer that in this + * class, the value of the `bar` slot should be a `SecondDerivedBar`. It also + * allows to bypass the dynamic check in the normal accessor above, if the + * compiler already knows that the assigned value is a FirstDerivedBar. + */ + public void setBar(SecondDerivedBar value) { + super.setBar(value); + } + + /* + * Overridden “standard” read accessor. + * + * We override it to ensure it returns the more specialised subtype. + * + * Here, since we know the slot cannot be further refined (no subclass), we can + * dispense with a wildcard generic. + */ + @Override + @SuppressWarnings("unchecked") + public List getBars() { + return (List) super.getBars(); + } + + /* + * Overriden read accessor with optional creation of the list. + * + * We must override this accessor to ensure that the created list (if the list + * needs to be created) is using the more specialised type. + */ + @Override + public List getBars(boolean create) { + // We can delegate the logic to the parent + return super.getBars(SecondDerivedBar.class, create); + } + + /* + * Overidden parameterised read accessor. + * + * In this class, we don’t need this accessor to get a modifiable list (we can + * use `getBars()` directly), but we must still override the accessor we inherit + * from the parent, otherwise this would allow client code to get a + * `List`-typed value. + */ + @Override + public List getBars(Class t) { + if ( !SecondDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t); + } + + /* + * Overridden parameterised read accessor with optional creation of the list. + * + * Same as above: we must override this accessor to add a runtime check on the + * type parameter. + */ + @Override + public List getBars(Class t, boolean create) { + if ( !SecondDerivedBar.class.isAssignableFrom(t) ) { + throw new IllegalArgumentException("Invalid type parameter"); + } + return super.getBars(t, create); + } + + /* + * Overridden “standard” write accessor. + * + * We must override this accessor to include a runtime check. The check must be + * performed on all list items. + */ + @Override + public void setBars(List value) { + for ( Bar b : value ) { + if ( !(b instanceof SecondDerivedBar) ) { + throw new IllegalArgumentException("Invalid bar value"); + } + } + // FIXME: the parent method will in turn perform a (no longer needed) runtime + // check... + super.setBars(value); + } +}