Skip to content

Commit dd10f2c

Browse files
committed
Add support for multivalued designator slots.
Now that we have the TypeDesignatorResolver helper class, we can plug it into the ObjectConverter to easily deal with both single- and multi-valued designator slots.
1 parent 1e9532e commit dd10f2c

4 files changed

Lines changed: 164 additions & 44 deletions

File tree

core/src/main/java/org/incenp/linkml/core/ObjectConverter.java

Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434

3535
package org.incenp.linkml.core;
3636

37-
import java.net.URI;
38-
import java.net.URISyntaxException;
3937
import java.util.ArrayList;
4038
import java.util.HashMap;
4139
import java.util.List;
@@ -57,7 +55,6 @@ public class ObjectConverter implements IConverter {
5755
private final static String STRING_EXPECTED = "Invalid value type, string expected";
5856
private final static String OBJECT_EXPECTED = "Invalid value type, '%s' expected";
5957
private final static String NO_IDENTIFIER = "Missing identifier for type '%s'";
60-
private final static String INVALID_CLASS_URI = "Missing or invalid class URI for type '%s'";
6158

6259
protected ClassInfo klass;
6360
private PrefixDeclarationExtractor prefixExtractor;
@@ -166,35 +163,23 @@ public Object convert(Map<String, Object> raw, ConverterContext ctx) throws Link
166163
Slot designatorSlot = klass.getTypeDesignatorSlot();
167164
Object designator = raw.get(designatorSlot.getLinkMLName());
168165
if ( designator != null ) {
169-
// FIXME: LinkML allows the type designator slot to be multivalued, in which
170-
// case we should pick "the most specific class" among the classes listed (what
171-
// to do if the list contains several classes at the same inheritance level is
172-
// unspecified). Currently we only support the single-valued case.
173-
String designatedName = ctx.getConverter(designatorSlot).convert(designator, ctx).toString();
174-
Class<?> designatedType = null;
175-
176-
// First try looking by class URI. This will only work if the designated class
177-
// has already been "seen" by the ClassInfo cache.
178-
ClassInfo ci = ClassInfo.get(designatedName);
179-
if ( ci != null ) {
180-
designatedType = ci.getType();
181-
} else {
182-
// No luck, so look up by class name.
183-
// FIXME: We assume that all classes derived from the initial class will live in
184-
// the same Java package. This is not ideal but, if all we have to refer to a
185-
// class is an unqualified name, it is not an unreasonable assumption.
186-
String pkgName = getType().getPackage().getName();
187-
try {
188-
designatedType = Class.forName(pkgName + "." + designatedName);
189-
} catch ( ClassNotFoundException e ) {
190-
// Do nothing, simply fallback to the type we were originally expecting. This is
191-
// so an application has a chance to process data that would use an unknown
192-
// subclass (possibly defined by an extension).
166+
TypeDesignatorResolver resolver = new TypeDesignatorResolver();
167+
IConverter designatorConverter = ctx.getConverter(designatorSlot);
168+
ClassInfo designatedClass = null;
169+
170+
if ( designatorSlot.isMultivalued() ) {
171+
ArrayList<String> designatorNames = new ArrayList<>();
172+
for ( Object rawDesignator : toList(designator) ) {
173+
designatorNames.add(designatorConverter.convert(rawDesignator, ctx).toString());
193174
}
175+
designatedClass = resolver.resolve(designatorNames, klass);
176+
} else {
177+
String designatedName = designatorConverter.convert(designator, ctx).toString();
178+
designatedClass = resolver.resolve(designatedName, klass);
194179
}
195180

196-
if ( designatedType != null ) {
197-
conv = (ObjectConverter) ctx.getConverter(designatedType);
181+
if ( designatedClass != null ) {
182+
conv = (ObjectConverter) ctx.getConverter(designatedClass.getType());
198183
}
199184
}
200185
}
@@ -354,22 +339,11 @@ public Map<String, Object> serialise(Object object, boolean withIdentifier, Conv
354339
Object slotValue = slot.getValue(object);
355340
if ( slotValue == null && slot.isTypeDesignator() ) {
356341
// Set the slot to the actual type name
357-
if ( slot.getInnerType().equals(URI.class) ) {
358-
// URI-typed designator slot, it requires the class URI
359-
try {
360-
slotValue = new URI(klass.getURI());
361-
} catch ( NullPointerException | URISyntaxException e ) {
362-
throw new LinkMLInternalError(String.format(INVALID_CLASS_URI, getType().getName()));
363-
}
364-
} else if ( ctx.getConverter(slot) instanceof CurieConverter ) {
365-
// URI-or-CURIE typed designator slot, also requiring the class URI
366-
if ( klass.getURI() == null ) {
367-
throw new LinkMLInternalError(String.format(INVALID_CLASS_URI, getType().getName()));
368-
}
369-
slotValue = klass.getURI();
342+
TypeDesignatorResolver resolver = new TypeDesignatorResolver();
343+
if ( slot.isMultivalued() ) {
344+
slotValue = resolver.getDesignators(klass);
370345
} else {
371-
// Assume a name-based type designator slot
372-
slotValue = getType().getSimpleName();
346+
slotValue = resolver.getDesignator(klass);
373347
}
374348
} else if ( slotValue == null ) {
375349
// Ignore all other empty slots

core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@
4343
import java.time.ZoneId;
4444
import java.time.ZonedDateTime;
4545
import java.util.ArrayList;
46+
import java.util.List;
4647
import java.util.Map;
4748

4849
import org.incenp.linkml.core.sample.BaseCurieSelfDesignatedClass;
50+
import org.incenp.linkml.core.sample.BaseMultiSelfDesignatedClass;
4951
import org.incenp.linkml.core.sample.BaseSelfDesignatedClass;
5052
import org.incenp.linkml.core.sample.BaseURISelfDesignatedClass;
5153
import org.incenp.linkml.core.sample.ClassWithCustomConverter;
@@ -57,6 +59,7 @@
5759
import org.incenp.linkml.core.sample.ContainerOfSimpleDicts;
5860
import org.incenp.linkml.core.sample.ContainerOfSimpleObjects;
5961
import org.incenp.linkml.core.sample.DerivedCurieSelfDesignatedClass;
62+
import org.incenp.linkml.core.sample.DerivedMultiSelfDesignatedClass;
6063
import org.incenp.linkml.core.sample.DerivedSelfDesignatedClass;
6164
import org.incenp.linkml.core.sample.DerivedURISelfDesignatedClass;
6265
import org.incenp.linkml.core.sample.ExtensibleSimpleClass;
@@ -424,6 +427,33 @@ void testUnknownTypeDesignator() throws IOException, LinkMLRuntimeException {
424427
roundtrip(dsdc);
425428
}
426429

430+
@Test
431+
void testMultivaluedTypeDesignator() throws IOException, LinkMLRuntimeException {
432+
String text = "foo: A string\nbar: Another string\ntype:\n - BaseMultiSelfDesignatedClass\n - DerivedMultiSelfDesignatedClass\n";
433+
BaseMultiSelfDesignatedClass bmsdc = parseString(text, BaseMultiSelfDesignatedClass.class);
434+
435+
Assertions.assertInstanceOf(DerivedMultiSelfDesignatedClass.class, bmsdc);
436+
DerivedMultiSelfDesignatedClass derived = (DerivedMultiSelfDesignatedClass) bmsdc;
437+
Assertions.assertEquals("A string", derived.getFoo());
438+
Assertions.assertEquals("Another string", derived.getBar());
439+
440+
roundtrip(derived);
441+
442+
// Try serialising again, but without the type designator; the slot should still
443+
// be set in the serialised object
444+
derived.setType(null);
445+
ObjectConverter conv = (ObjectConverter) ctx.getConverter(derived.getClass());
446+
Map<String, Object> raw = conv.serialise(derived, true, ctx);
447+
Object rawType = raw.get("type");
448+
Assertions.assertNotNull(rawType);
449+
Assertions.assertInstanceOf(List.class, rawType);
450+
@SuppressWarnings("unchecked")
451+
List<String> types = (List<String>) rawType;
452+
Assertions.assertEquals(2, types.size());
453+
Assertions.assertEquals("BaseMultiSelfDesignatedClass", types.get(0));
454+
Assertions.assertEquals("DerivedMultiSelfDesignatedClass", types.get(1));
455+
}
456+
427457
@Test
428458
void testReferenceToIRIIdentifiers() throws IOException, LinkMLRuntimeException {
429459
ctx.addPrefix("PFX", "https://example.org/");
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* LinkML-Java - LinkML library for Java
3+
* Copyright © 2026 Damien Goutte-Gattat
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* (1) Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* (2) Redistributions in binary form must reproduce the above
13+
* copyright notice, this list of conditions and the following
14+
* disclaimer in the documentation and/or other materials provided
15+
* with the distribution.
16+
*
17+
* (3) Neither the name of the copyright holder nor the names its
18+
* contributors may be used to endorse or promote products derived
19+
* from this software without specific prior written permission.
20+
*
21+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
22+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
31+
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32+
* POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
package org.incenp.linkml.core.sample;
36+
37+
import java.util.List;
38+
import java.util.Map;
39+
40+
import org.incenp.linkml.core.annotations.ExtensionHolder;
41+
import org.incenp.linkml.core.annotations.TypeDesignator;
42+
43+
import lombok.AccessLevel;
44+
import lombok.AllArgsConstructor;
45+
import lombok.Data;
46+
import lombok.NoArgsConstructor;
47+
import lombok.experimental.SuperBuilder;
48+
49+
/**
50+
* An example of a class that has a multi-valued type designator slot.
51+
*/
52+
@NoArgsConstructor
53+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
54+
@SuperBuilder(toBuilder = true)
55+
@Data
56+
public class BaseMultiSelfDesignatedClass {
57+
private String foo;
58+
@TypeDesignator
59+
private List<String> type;
60+
@ExtensionHolder
61+
private Map<String, Object> extraSlots;
62+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* LinkML-Java - LinkML library for Java
3+
* Copyright © 2026 Damien Goutte-Gattat
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions
7+
* are met:
8+
*
9+
* (1) Redistributions of source code must retain the above copyright
10+
* notice, this list of conditions and the following disclaimer.
11+
*
12+
* (2) Redistributions in binary form must reproduce the above
13+
* copyright notice, this list of conditions and the following
14+
* disclaimer in the documentation and/or other materials provided
15+
* with the distribution.
16+
*
17+
* (3) Neither the name of the copyright holder nor the names its
18+
* contributors may be used to endorse or promote products derived
19+
* from this software without specific prior written permission.
20+
*
21+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS
22+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25+
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26+
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27+
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28+
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29+
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30+
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
31+
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32+
* POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
package org.incenp.linkml.core.sample;
36+
37+
import lombok.AccessLevel;
38+
import lombok.AllArgsConstructor;
39+
import lombok.Data;
40+
import lombok.EqualsAndHashCode;
41+
import lombok.NoArgsConstructor;
42+
import lombok.experimental.SuperBuilder;
43+
44+
/**
45+
* An example of a class that derives from a class with a multi-valued type designator slot.
46+
*/
47+
@NoArgsConstructor
48+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
49+
@SuperBuilder(toBuilder = true)
50+
@Data
51+
@EqualsAndHashCode(callSuper = true)
52+
public class DerivedMultiSelfDesignatedClass extends BaseMultiSelfDesignatedClass {
53+
private String bar;
54+
}

0 commit comments

Comments
 (0)