Skip to content
Merged
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
62 changes: 54 additions & 8 deletions core/src/main/java/org/incenp/linkml/core/Slot.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -79,13 +81,30 @@ public class Slot {
* fide</em> LinkML object).
*/
public Slot(Field field) throws LinkMLRuntimeException {
this(field, field.getDeclaringClass());
}

/**
* Creates a new instance.
*
* @param field The Java field that represents the slot.
* @param klass The class to which the slot belongs.
* @throws LinkMLRuntimeException If neither the class the field belongs to or
* any of its parents declare accessor methods
* for the slot (which should not happen if the
* class is a <em>bona fide</em> LinkML object).
*/
public Slot(Field field, Class<?> klass) throws LinkMLRuntimeException {
this.field = field;
outerType = field.getType();

Class<?> klass = field.getDeclaringClass();
try {
writeAccessor = klass.getDeclaredMethod(getWriteAccessorName(field), new Class<?>[] { outerType });
readAccessor = klass.getDeclaredMethod(getReadAccessorName(field), (Class<?>[]) null);
readAccessor = klass.getMethod(getReadAccessorName(field), (Class<?>[]) null);
} catch ( NoSuchMethodException | SecurityException e ) {
throw new LinkMLInternalError(String.format("Missing accessor for slot '%s'", field.getName()), e);
}
outerType = readAccessor.getReturnType();
try {
writeAccessor = klass.getMethod(getWriteAccessorName(field), new Class<?>[] { outerType });
} catch ( NoSuchMethodException | SecurityException e ) {
throw new LinkMLInternalError(String.format("Missing accessor for slot '%s'", field.getName()), e);
}
Expand Down Expand Up @@ -210,8 +229,13 @@ public boolean isCurieTyped() {
*/
public Class<?> getInnerType() {
if ( isMultivalued() ) {
ParameterizedType pt = (ParameterizedType) field.getGenericType();
return (Class<?>) pt.getActualTypeArguments()[0];
ParameterizedType pt = (ParameterizedType) readAccessor.getGenericReturnType();
Type t = pt.getActualTypeArguments()[0];
if ( t instanceof WildcardType ) {
return (Class<?>) ((WildcardType) t).getUpperBounds()[0];
} else {
return (Class<?>) t;
}
}
return outerType;
}
Expand Down Expand Up @@ -327,6 +351,28 @@ public Class<?> getDeclaringClass() {
return field.getDeclaringClass();
}

/**
* Gets the class in which the slot is refined, if any.
* <p>
* In LinkML, a class that inherit a slot from one of its parents can
* <em>refine</em> that slot by restricting its range to a more specific class
* than the original range.
* <p>
* In this runtime, we detect this by looking for a read accessor in one of the
* derived classes, that overrides the original read accessor in the class that
* defines the slot.
* <p>
* If the slot is refined by several classes successively, this method will
* return the last refining class.
*
* @return The class that refines the slot, or <code>null</code> if the slot is
* not refined relatively to its declaring class.
*/
public Class<?> getRefiningClass() {
Class<?> klass = readAccessor.getDeclaringClass();
return klass != getDeclaringClass() ? klass : null;
}

/**
* Assigns a value to the slot for the given object.
*
Expand Down Expand Up @@ -440,7 +486,7 @@ public static Slot getSlot(Class<?> klass, String name) throws LinkMLRuntimeExce
do {
try {
Field f = current.getDeclaredField(name);
return new Slot(f);
return new Slot(f, klass);
} catch ( NoSuchFieldException e ) {
}

Expand All @@ -463,7 +509,7 @@ public static Collection<Slot> getSlots(Class<?> klass) {
do {
for ( Field f : current.getDeclaredFields() ) {
try {
slots.add(new Slot(f));
slots.add(new Slot(f, klass));
} catch ( LinkMLRuntimeException e ) {
// Assume this is not a LinkML field
}
Expand Down
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;
}
}
Loading
Loading