Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions core/src/test/java/org/incenp/linkml/core/playground/Bar.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.incenp.linkml.core.playground;

/**
* An example of a class that is used in a “refined” slot.
* <p>
* This class is used in the {@link Foo} class. Some of the classes that are
* derived from <code>Foo</code> uses derived classes instead.
*/
public class Bar {
private String name;

public String getName() {
return name;
}

public void setName(String value) {
name = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.incenp.linkml.core.playground;

/**
* First derived class from Bar.
* <p>
* This class is used, instead of its parent <code>Bar</code> in
* {@link FirstDerivedFoo}.
*/
public class FirstDerivedBar extends Bar {

private int length;

public int getLength() {
return length;
}

public void setLength(int value) {
length = value;
}
}
Original file line number Diff line number Diff line change
@@ -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<Bar>`.
return (List<FirstDerivedBar>) 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 <T extends Bar> List<T> getBars(Class<T> 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 <T extends Bar> List<T> getBars(Class<T> 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);
}

}
100 changes: 100 additions & 0 deletions core/src/test/java/org/incenp/linkml/core/playground/Foo.java
Original file line number Diff line number Diff line change
@@ -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<Bar>) 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<Bar>();
}
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 <T extends Bar> List<T> getBars(Class<T> t) {
return (List<T>) 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 <T extends Bar> List<T> getBars(Class<T> t, boolean create) {
if ( bars == null && create ) {
bars = new ArrayList<T>();
}
return (List<T>) bars;
}

/*
* “Standard” write accessor.
*/
public void setBars(List<? extends Bar> value) {
bars = value;
}
}
115 changes: 115 additions & 0 deletions core/src/test/java/org/incenp/linkml/core/playground/Playground.java
Original file line number Diff line number Diff line change
@@ -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]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.incenp.linkml.core.playground;

/**
* Second derived class from Bar.
* <p>
* This class is used, instead of its parent <code>FirstDerivedBar</code>, in
* {@link ThirdDerivedFoo}.
*/
public class SecondDerivedBar extends FirstDerivedBar {

}
Loading
Loading