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 extends FirstDerivedBar> 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 extends FirstDerivedBar> 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 extends Bar> 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 extends Bar> 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 extends Bar> 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 extends Bar> 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 extends Bar> 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 extends Bar> 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);
+ }
+}