From 74012a56ed70f5b7d3dd859256b61a032f8d9d46 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 25 Sep 2026 16:25:28 +0100 Subject: [PATCH 1/5] Explicitly map EntityReference to String. With the recent changes to the Java generator in LinkML-Py, EntityReference-typed slots are no longer automatically mapped to String - they are mapped to URI instead. We need to explicitly override that. --- core/src/main/linkml/scripts/javagen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/linkml/scripts/javagen.py b/core/src/main/linkml/scripts/javagen.py index c9d7c5b..40c2fc9 100644 --- a/core/src/main/linkml/scripts/javagen.py +++ b/core/src/main/linkml/scripts/javagen.py @@ -12,6 +12,7 @@ "predicate_modifier_enum": "PredicateModifier", "sssom_version_enum": "Version", "NonRelativeURI": "String", + "EntityReference": "String", } From dc248a513f509786d7b2b7f026a7e873da5cddb9 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 25 Sep 2026 16:29:16 +0100 Subject: [PATCH 2/5] Support "discouraged condensation" for some slots. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSSOM spec may (or not…) soon mark some slots as being propagatable while recommending against their condensation. We support this by making the `Propagatable` field annotation accept an optional boolean parameter (`condensationDiscouraged`). A slot marked with `Propagatable(condensationDiscouraged=true)` will always be propagated as any other propagatable slot, but by default will not be condensed. --- .../obofoundry/sssom/model/Propagatable.java | 2 +- .../incenp/obofoundry/sssom/slots/Slot.java | 12 +++++ .../sssom/slots/SlotPropagator.java | 46 +++++++++++++++++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/model/Propagatable.java b/core/src/main/java/org/incenp/obofoundry/sssom/model/Propagatable.java index 1e93d1c..de60b35 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/model/Propagatable.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/model/Propagatable.java @@ -33,5 +33,5 @@ @Retention(RUNTIME) @Target(FIELD) public @interface Propagatable { - + boolean condensationDiscouraged() default false; } diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/slots/Slot.java b/core/src/main/java/org/incenp/obofoundry/sssom/slots/Slot.java index 3160976..1c4b9de 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/slots/Slot.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/slots/Slot.java @@ -121,6 +121,18 @@ public boolean isPropagatable(Version inVersion) { return field.isAnnotationPresent(Propagatable.class) && getCompliantVersion().isCompatibleWith(inVersion); } + /** + * Indicates whether the SSSOM specification recommends against condensing this + * slot. + * + * @return {@code true} if condensing the slot is discouraged by the + * specification, otherwise {@code false}. + */ + public boolean isCondensationDiscouraged() { + Propagatable annot = field.getAnnotation(Propagatable.class); + return annot != null ? annot.condensationDiscouraged() : false; + } + /** * Indicates whether the slot is expected to contain a URI. * diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/slots/SlotPropagator.java b/core/src/main/java/org/incenp/obofoundry/sssom/slots/SlotPropagator.java index 7932115..fc04f69 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/slots/SlotPropagator.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/slots/SlotPropagator.java @@ -65,6 +65,7 @@ public class SlotPropagator { private Set slots = null; private PropagationPolicy policy = PropagationPolicy.AlwaysReplace; private Version maxVersion = Version.LATEST; + private boolean forceCondensation = false; /** * Creates a new instance using the default propagation policy (always replace). @@ -95,6 +96,24 @@ public SlotPropagator(PropagationPolicy policy, Version maxVersion) { this.maxVersion = maxVersion; } + /** + * Creates a new instance with the specified propagation policy, highest + * version, and forcing behaviour. + * + * @param policy The default propagation policy to use. + * @param maxVersion The default highest version of the SSSOM specification that + * sets should remain compliant with after propagation or + * condensation. + * @param force If true, slots that the SSSOM specification + * recommends should not be condensed will be condensed + * anyway. + */ + public SlotPropagator(PropagationPolicy policy, Version maxVersion, boolean force) { + this.policy = policy; + this.maxVersion = maxVersion; + this.forceCondensation = force; + } + /** * Sets the propagation policy to use. The new policy will be used in all * subsequent calls to {@link #propagate(MappingSet)} and @@ -124,6 +143,21 @@ public void setMaxVersion(Version maxVersion) { } } + /** + * Sets the forcing condensation behaviour. + *

+ * The SSSOM specification may explicitly discourages the condensation of some + * slots, even if they are marked as propagatable. By default, this object does + * not condense any such slot. This method allows to change this behaviour to + * force the condensation of all propagatable slots. + * + * @param force If true, slots that the SSSOM specification + * recommends should not be condensed will be condensed anyway. + */ + public void setForceCondensation(boolean force) { + forceCondensation = force; + } + /** * Propagates the values of slots from the mapping set level to the individual * mappings. @@ -153,7 +187,7 @@ public Set propagate(MappingSet mappingSet, boolean preserve) { // set level. Map values = new HashMap(); SlotHelper setHelper = SlotHelper.getMappingSetHelper(true); - setHelper.setSlots(getSlots()); // Visit only propagatable slots + setHelper.setSlots(getSlots(true)); // Visit only propagatable slots setHelper.visitSlots(mappingSet, (slot, m, value) -> values.put(slot.getName(), value)); // Prepare to visit the slots on the individual mappings. We only need to visit @@ -224,7 +258,7 @@ public Set condense(MappingSet mappingSet, boolean preserve) { // slots. Map> values = new HashMap>(); SlotHelper mappingHelper = SlotHelper.getMappingHelper(true); - mappingHelper.setSlots(new ArrayList(getSlots()), false); + mappingHelper.setSlots(new ArrayList(getSlots(forceCondensation)), false); for ( Mapping mapping : mappingSet.getMappings() ) { mappingHelper.visitSlots(mapping, (slot, m, value) -> values .computeIfAbsent(slot.getName(), (s) -> new HashSet()).add(value), true); @@ -233,7 +267,7 @@ public Set condense(MappingSet mappingSet, boolean preserve) { // Visit the condensable slots on the mapping set level and set them to the // corresponding collected value. SlotHelper setHelper = SlotHelper.getMappingSetHelper(true); - setHelper.setSlots(getSlots()); + setHelper.setSlots(getSlots(forceCondensation)); Set condensedSlots = new HashSet(); ISimpleSlotVisitor v = (slot, ms, value) -> { String slotName = slot.getName(); @@ -274,12 +308,14 @@ public Set condense(MappingSet mappingSet, boolean preserve) { return condensedSlots; } - private Set getSlots() { + private Set getSlots(boolean force) { if ( slots == null ) { slots = new HashSet<>(); for ( Slot slot : SlotHelper.getMappingSetHelper().getSlots() ) { if ( slot.isPropagatable(maxVersion) ) { - slots.add(slot.getName()); + if ( force || !slot.isCondensationDiscouraged() ) { + slots.add(slot.getName()); + } } } } From 24f37ead4a43525349ff829bb0350b16bd32bf71 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 25 Sep 2026 16:35:50 +0100 Subject: [PATCH 3/5] Add `annotation_discouraged` annotation to the LinkML schema. Add a mechanism to mark slots whose condensation is to be discouraged directly within the SSSOM schema. Update the Java generator and the template accordingly to exploit that annotation when present. --- .../org/incenp/obofoundry/sssom/model/sssom.yaml | 5 +++++ core/src/main/linkml/scripts/javagen.py | 12 ++++++++++++ core/src/main/linkml/templates/class.jinja2 | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml b/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml index d1949e3..f5b2baa 100644 --- a/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml +++ b/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml @@ -1496,6 +1496,11 @@ classes: Indicates whether a slot can be propagated from a mapping down to individual mappings. range: boolean + condensation_discouraged: + description: + If true, this indicates that, while the slot is propagatable, SSSOM + writers SHOULD avoid condensing it. + range: boolean Versionable: class_uri: sssom:Versionable description: diff --git a/core/src/main/linkml/scripts/javagen.py b/core/src/main/linkml/scripts/javagen.py index 40c2fc9..afe95c8 100644 --- a/core/src/main/linkml/scripts/javagen.py +++ b/core/src/main/linkml/scripts/javagen.py @@ -67,6 +67,17 @@ def is_propagatable(self, slot_name): return "propagated" in d return False + def is_condensation_discouraged(self, slot_name): + """Check if the spec recommends against condensing the slot. + + :param slot_name: the name of the slot to check. + """ + + d = self.schemaview.annotation_dict(slot_name) + if d is not None: + return "condensation_discouraged" in d + return False + def get_added_in_version(self, slot_name, class_name): """Get the added_in annotation carried by this slot, if any. @@ -96,6 +107,7 @@ def get_range(self, field): :param field: The field object produced by the code generator. """ + print(f"get_range: {field.source_slot.range}") custom = custom_types.get(field.source_slot.range, None) if custom: if field.source_slot.multivalued: diff --git a/core/src/main/linkml/templates/class.jinja2 b/core/src/main/linkml/templates/class.jinja2 index 2b8bc07..7ac1e37 100644 --- a/core/src/main/linkml/templates/class.jinja2 +++ b/core/src/main/linkml/templates/class.jinja2 @@ -48,7 +48,7 @@ public class {{ cls.name }} {% if cls.is_a -%} extends {{ cls.is_a }} {%- endif @EntityReference {%- endif %} {%- if cls.name == 'MappingSet' and gen.is_propagatable(f.source_slot.name) %} - @Propagatable + @Propagatable{% if gen.is_condensation_discouraged(f.source_slot.name) %}(condensationDiscouraged=true){% endif %} {%- endif %} {%- if f.source_slot.slot_uri %} @SlotURI("{{ f.slot_uri }}") From 8543f8f914124eab4b90ec5dd45016072d251940 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 25 Sep 2026 16:36:53 +0100 Subject: [PATCH 4/5] Make predicate_id and `mapping_justification` propagatable. As currently under discussion in the SSSOM spec, we mark the predicate_id and mapping_justification slots (as well as the predicate_label slot) as being propagatable but with the caveat that their condensation is discouraged. --- .../obofoundry/sssom/model/MappingSet.java | 114 ++++++++++++++++++ .../incenp/obofoundry/sssom/model/sssom.yaml | 32 +++++ .../sssom/slots/SlotPropagatorTest.java | 30 +++++ .../obofoundry/sssom/slots/SlotTest.java | 14 +++ 4 files changed, 190 insertions(+) diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java b/core/src/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java index cd718c8..134df1e 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java @@ -104,6 +104,18 @@ public class MappingSet { @Propagatable private String objectSourceVersion; + @JsonProperty("predicate_id") + @EntityReference + @Propagatable(condensationDiscouraged=true) + @SlotURI("http://www.w3.org/2002/07/owl#annotatedProperty") + @Versionable(addedIn = Version.SSSOM_1_1) + private String predicateId; + + @JsonProperty("predicate_label") + @Propagatable + @Versionable(addedIn = Version.SSSOM_1_1) + private String predicateLabel; + @JsonProperty("predicate_type") @Propagatable @Versionable(addedIn = Version.SSSOM_1_1) @@ -138,6 +150,12 @@ public class MappingSet { @SlotURI("http://purl.org/dc/terms/created") private LocalDate mappingDate; + @JsonProperty("mapping_justification") + @EntityReference + @Propagatable(condensationDiscouraged=true) + @Versionable(addedIn = Version.SSSOM_1_1) + private String mappingJustification; + @JsonProperty("publication_date") @SlotURI("http://purl.org/dc/terms/issued") private LocalDate publicationDate; @@ -224,6 +242,8 @@ protected MappingSet(final Version sssomVersion, final EntityType objectType, final String objectSource, final String objectSourceVersion, + final String predicateId, + final String predicateLabel, final EntityType predicateType, final String mappingProvider, final List cardinalityScope, @@ -231,6 +251,7 @@ protected MappingSet(final Version sssomVersion, final String mappingToolId, final String mappingToolVersion, final LocalDate mappingDate, + final String mappingJustification, final LocalDate publicationDate, final List subjectMatchField, final List objectMatchField, @@ -263,6 +284,8 @@ protected MappingSet(final Version sssomVersion, this.objectType = objectType; this.objectSource = objectSource; this.objectSourceVersion = objectSourceVersion; + this.predicateId = predicateId; + this.predicateLabel = predicateLabel; this.predicateType = predicateType; this.mappingProvider = mappingProvider; this.cardinalityScope = cardinalityScope; @@ -270,6 +293,7 @@ protected MappingSet(final Version sssomVersion, this.mappingToolId = mappingToolId; this.mappingToolVersion = mappingToolVersion; this.mappingDate = mappingDate; + this.mappingJustification = mappingJustification; this.publicationDate = publicationDate; this.subjectMatchField = subjectMatchField; this.objectMatchField = objectMatchField; @@ -618,6 +642,34 @@ public void setObjectSourceVersion(final String value) { this.objectSourceVersion = value; } + /** + * Gets the value of the predicate_id slot. + */ + public String getPredicateId() { + return this.predicateId; + } + + /** + * Sets the value of the predicate_id slot. + */ + public void setPredicateId(final String value) { + this.predicateId = value; + } + + /** + * Gets the value of the predicate_label slot. + */ + public String getPredicateLabel() { + return this.predicateLabel; + } + + /** + * Sets the value of the predicate_label slot. + */ + public void setPredicateLabel(final String value) { + this.predicateLabel = value; + } + /** * Gets the value of the predicate_type slot. */ @@ -731,6 +783,20 @@ public void setMappingDate(final LocalDate value) { this.mappingDate = value; } + /** + * Gets the value of the mapping_justification slot. + */ + public String getMappingJustification() { + return this.mappingJustification; + } + + /** + * Sets the value of the mapping_justification slot. + */ + public void setMappingJustification(final String value) { + this.mappingJustification = value; + } + /** * Gets the value of the publication_date slot. */ @@ -1156,6 +1222,16 @@ public String toString() { sb.append(this.objectSourceVersion); sb.append(","); } + if ( this.predicateId != null ) { + sb.append("predicate_id="); + sb.append(this.predicateId); + sb.append(","); + } + if ( this.predicateLabel != null ) { + sb.append("predicate_label="); + sb.append(this.predicateLabel); + sb.append(","); + } if ( this.predicateType != null ) { sb.append("predicate_type="); sb.append(this.predicateType); @@ -1191,6 +1267,11 @@ public String toString() { sb.append(this.mappingDate); sb.append(","); } + if ( this.mappingJustification != null ) { + sb.append("mapping_justification="); + sb.append(this.mappingJustification); + sb.append(","); + } if ( this.publicationDate != null ) { sb.append("publication_date="); sb.append(this.publicationDate); @@ -1294,6 +1375,8 @@ public boolean equals(final Object o) { if ( this.objectType == null ? other.objectType != null : !this.objectType.equals(other.objectType)) return false; if ( this.objectSource == null ? other.objectSource != null : !this.objectSource.equals(other.objectSource)) return false; if ( this.objectSourceVersion == null ? other.objectSourceVersion != null : !this.objectSourceVersion.equals(other.objectSourceVersion)) return false; + if ( this.predicateId == null ? other.predicateId != null : !this.predicateId.equals(other.predicateId)) return false; + if ( this.predicateLabel == null ? other.predicateLabel != null : !this.predicateLabel.equals(other.predicateLabel)) return false; if ( this.predicateType == null ? other.predicateType != null : !this.predicateType.equals(other.predicateType)) return false; if ( this.mappingProvider == null ? other.mappingProvider != null : !this.mappingProvider.equals(other.mappingProvider)) return false; if ( this.cardinalityScope == null ? other.cardinalityScope != null : !this.cardinalityScope.equals(other.cardinalityScope)) return false; @@ -1301,6 +1384,7 @@ public boolean equals(final Object o) { if ( this.mappingToolId == null ? other.mappingToolId != null : !this.mappingToolId.equals(other.mappingToolId)) return false; if ( this.mappingToolVersion == null ? other.mappingToolVersion != null : !this.mappingToolVersion.equals(other.mappingToolVersion)) return false; if ( this.mappingDate == null ? other.mappingDate != null : !this.mappingDate.equals(other.mappingDate)) return false; + if ( this.mappingJustification == null ? other.mappingJustification != null : !this.mappingJustification.equals(other.mappingJustification)) return false; if ( this.publicationDate == null ? other.publicationDate != null : !this.publicationDate.equals(other.publicationDate)) return false; if ( this.subjectMatchField == null ? other.subjectMatchField != null : !this.subjectMatchField.equals(other.subjectMatchField)) return false; if ( this.objectMatchField == null ? other.objectMatchField != null : !this.objectMatchField.equals(other.objectMatchField)) return false; @@ -1344,6 +1428,8 @@ public int hashCode() { result = result * PRIME + (this.objectType == null ? 43 : this.objectType.hashCode()); result = result * PRIME + (this.objectSource == null ? 43 : this.objectSource.hashCode()); result = result * PRIME + (this.objectSourceVersion == null ? 43 : this.objectSourceVersion.hashCode()); + result = result * PRIME + (this.predicateId == null ? 43 : this.predicateId.hashCode()); + result = result * PRIME + (this.predicateLabel == null ? 43 : this.predicateLabel.hashCode()); result = result * PRIME + (this.predicateType == null ? 43 : this.predicateType.hashCode()); result = result * PRIME + (this.mappingProvider == null ? 43 : this.mappingProvider.hashCode()); result = result * PRIME + (this.cardinalityScope == null ? 43 : this.cardinalityScope.hashCode()); @@ -1351,6 +1437,7 @@ public int hashCode() { result = result * PRIME + (this.mappingToolId == null ? 43 : this.mappingToolId.hashCode()); result = result * PRIME + (this.mappingToolVersion == null ? 43 : this.mappingToolVersion.hashCode()); result = result * PRIME + (this.mappingDate == null ? 43 : this.mappingDate.hashCode()); + result = result * PRIME + (this.mappingJustification == null ? 43 : this.mappingJustification.hashCode()); result = result * PRIME + (this.publicationDate == null ? 43 : this.publicationDate.hashCode()); result = result * PRIME + (this.subjectMatchField == null ? 43 : this.subjectMatchField.hashCode()); result = result * PRIME + (this.objectMatchField == null ? 43 : this.objectMatchField.hashCode()); @@ -1387,6 +1474,8 @@ public static class MappingSetBuilder { private EntityType objectType; private String objectSource; private String objectSourceVersion; + private String predicateId; + private String predicateLabel; private EntityType predicateType; private String mappingProvider; private List cardinalityScope; @@ -1394,6 +1483,7 @@ public static class MappingSetBuilder { private String mappingToolId; private String mappingToolVersion; private LocalDate mappingDate; + private String mappingJustification; private LocalDate publicationDate; private List subjectMatchField; private List objectMatchField; @@ -1502,6 +1592,16 @@ public MappingSet.MappingSetBuilder objectSourceVersion(final String objectSourc return this; } + public MappingSet.MappingSetBuilder predicateId(final String predicateId) { + this.predicateId = predicateId; + return this; + } + + public MappingSet.MappingSetBuilder predicateLabel(final String predicateLabel) { + this.predicateLabel = predicateLabel; + return this; + } + public MappingSet.MappingSetBuilder predicateType(final EntityType predicateType) { this.predicateType = predicateType; return this; @@ -1537,6 +1637,11 @@ public MappingSet.MappingSetBuilder mappingDate(final LocalDate mappingDate) { return this; } + public MappingSet.MappingSetBuilder mappingJustification(final String mappingJustification) { + this.mappingJustification = mappingJustification; + return this; + } + public MappingSet.MappingSetBuilder publicationDate(final LocalDate publicationDate) { this.publicationDate = publicationDate; return this; @@ -1626,6 +1731,8 @@ public MappingSet build() { this.objectType, this.objectSource, this.objectSourceVersion, + this.predicateId, + this.predicateLabel, this.predicateType, this.mappingProvider, this.cardinalityScope, @@ -1633,6 +1740,7 @@ public MappingSet build() { this.mappingToolId, this.mappingToolVersion, this.mappingDate, + this.mappingJustification, this.publicationDate, this.subjectMatchField, this.objectMatchField, @@ -1668,6 +1776,8 @@ public String toString() { + ", objectType=" + this.objectType + ", objectSource=" + this.objectSource + ", objectSourceVersion=" + this.objectSourceVersion + + ", predicateId=" + this.predicateId + + ", predicateLabel=" + this.predicateLabel + ", predicateType=" + this.predicateType + ", mappingProvider=" + this.mappingProvider + ", cardinalityScope=" + this.cardinalityScope @@ -1675,6 +1785,7 @@ public String toString() { + ", mappingToolId=" + this.mappingToolId + ", mappingToolVersion=" + this.mappingToolVersion + ", mappingDate=" + this.mappingDate + + ", mappingJustification=" + this.mappingJustification + ", publicationDate=" + this.publicationDate + ", subjectMatchField=" + this.subjectMatchField + ", objectMatchField=" + this.objectMatchField @@ -1716,6 +1827,8 @@ public MappingSet.MappingSetBuilder toBuilder() { .objectType(this.objectType) .objectSource(this.objectSource) .objectSourceVersion(this.objectSourceVersion) + .predicateId(this.predicateId) + .predicateLabel(this.predicateLabel) .predicateType(this.predicateType) .mappingProvider(this.mappingProvider) .cardinalityScope(this.cardinalityScope) @@ -1723,6 +1836,7 @@ public MappingSet.MappingSetBuilder toBuilder() { .mappingToolId(this.mappingToolId) .mappingToolVersion(this.mappingToolVersion) .mappingDate(this.mappingDate) + .mappingJustification(this.mappingJustification) .publicationDate(this.publicationDate) .subjectMatchField(this.subjectMatchField) .objectMatchField(this.objectMatchField) diff --git a/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml b/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml index f5b2baa..ffe066a 100644 --- a/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml +++ b/core/src/main/linkml/schemas/org/incenp/obofoundry/sssom/model/sssom.yaml @@ -309,6 +309,11 @@ slots: mappings: - owl:annotatedProperty range: EntityReference + instantiates: + - sssom:Propagatable + annotations: + propagated: true + condensation_discouraged: true required: true slot_uri: owl:annotatedProperty examples: @@ -380,6 +385,10 @@ slots: predicate_label: description: The label of the predicate/relation of the mapping. range: string + instantiates: + - sssom:Propagatable + annotations: + propagated: true examples: - value: has cross-reference description: @@ -447,6 +456,11 @@ slots: A mapping justification is an action (or the written representation of that action) of showing a mapping to be right or reasonable. range: EntityReference + instantiates: + - sssom:Propagatable + annotations: + propagated: true + condensation_discouraged: true pattern: "^semapv:(MappingReview|ManualMappingCuration|LogicalReasoning|LexicalMatching|CompositeMatching|UnspecifiedMatching|SemanticSimilarityThresholdMatching|LexicalSimilarityThresholdMatching|MappingChaining|MappingInversion|StructuralMatching|InstanceBasedMatching|BackgroundKnowledgeBasedMatching)$" required: true any_of: @@ -1266,6 +1280,21 @@ classes: - sssom:Versionable annotations: added_in: "1.1" + predicate_id: + instantiates: + - sssom:Versionable + annotations: + added_in: "1.1" + predicate_label: + instantiates: + - sssom:Versionable + annotations: + added_in: "1.1" + mapping_justification: + instantiates: + - sssom:Versionable + annotations: + added_in: "1.1" slots: - sssom_version - curie_map @@ -1285,6 +1314,8 @@ classes: - object_type - object_source - object_source_version + - predicate_id + - predicate_label - predicate_type - mapping_provider - cardinality_scope @@ -1292,6 +1323,7 @@ classes: - mapping_tool_id - mapping_tool_version - mapping_date + - mapping_justification - publication_date - subject_match_field - object_match_field diff --git a/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java index d4a865b..8d1fb32 100644 --- a/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java +++ b/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java @@ -359,6 +359,36 @@ void testCondenseAlreadyCondensed() { Assertions.assertNull(ms.getMappings().get(0).getMappingTool()); } + @Test + void testCondenseCoreSlots() { + MappingSet ms = getSampleSet(); + SlotPropagator sp = new SlotPropagator(); + sp.setForceCondensation(true); + + Set condensed = sp.condense(ms, false); + Assertions.assertTrue(condensed.contains("predicate_id")); + Assertions.assertEquals(CommonPredicate.SKOS_EXACT_MATCH.toString(), ms.getPredicateId()); + Assertions.assertNull(ms.getMappings().get(0).getPredicateId()); + } + + @Test + void testPropagateCoreSlots() { + MappingSet ms = getSampleSet(); + ms.setPredicateId(ms.getMappings().get(0).getPredicateId()); + for ( Mapping m : ms.getMappings() ) { + m.setPredicateId(null); + } + + SlotPropagator sp = new SlotPropagator(); + + // Core slots should always be propagated + Set propagated = sp.propagate(ms); + sp.setForceCondensation(false); + Assertions.assertTrue(propagated.contains("predicate_id")); + Assertions.assertNull(ms.getPredicateId()); + Assertions.assertEquals(CommonPredicate.SKOS_EXACT_MATCH.toString(), ms.getMappings().get(0).getPredicateId()); + } + private MappingSet getSampleSet() { // @formatter:off MappingSet ms = MappingSet.builder() diff --git a/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotTest.java index ac281c9..bcf9952 100644 --- a/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotTest.java +++ b/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotTest.java @@ -58,4 +58,18 @@ void testPropagatableSlotForVersion() { Assertions.assertFalse(slot.isPropagatable(Version.SSSOM_1_1)); Assertions.assertFalse(slot.isPropagatable(Version.UNKNOWN)); } + + @Test + void testCondensationDiscouraged() { + SlotHelper helper = SlotHelper.getMappingSetHelper(); + Slot slot; + + slot = helper.getSlotByName("subject_type"); + Assertions.assertTrue(slot.isPropagatable()); + Assertions.assertFalse(slot.isCondensationDiscouraged()); + + slot = helper.getSlotByName("predicate_id"); + Assertions.assertTrue(slot.isPropagatable()); + Assertions.assertTrue(slot.isCondensationDiscouraged()); + } } From 174a508191fa1e368170db8893d77a5a85d5931d Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 25 Sep 2026 20:54:48 +0100 Subject: [PATCH 5/5] Make condensation of core slots configurable. Add a `setForceCondensation()` method to SSSOMWriter to allow client code to force the writer to condense all propagatable slots, even those that should not be condensed. The default behaviour remains not to condense those slots. Also expose that settings to the command line tool, in the form of a new option `--condense-all-slots`. --- .../obofoundry/sssom/cli/SimpleCLI.java | 6 +++++ .../incenp/obofoundry/sssom/JSONWriter.java | 3 +-- .../incenp/obofoundry/sssom/SSSOMWriter.java | 24 ++++++++++++++++- .../obofoundry/sssom/JSONWriterTest.java | 6 +++++ .../obofoundry/sssom/TSVReaderTest.java | 15 +++++++++++ .../obofoundry/sssom/TSVWriterTest.java | 17 ++++++++++++ .../test-condensed-core-slots.sssom.json | 27 +++++++++++++++++++ .../test-condensed-core-slots.sssom.tsv | 10 +++++++ .../sets/exo2c-core-slots-condensed.sssom.tsv | 23 ++++++++++++++++ 9 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 core/src/test/resources/output/test-condensed-core-slots.sssom.json create mode 100644 core/src/test/resources/output/test-condensed-core-slots.sssom.tsv create mode 100644 core/src/test/resources/sets/exo2c-core-slots-condensed.sssom.tsv diff --git a/cli/src/main/java/org/incenp/obofoundry/sssom/cli/SimpleCLI.java b/cli/src/main/java/org/incenp/obofoundry/sssom/cli/SimpleCLI.java index 9179b7b..ee70fb8 100644 --- a/cli/src/main/java/org/incenp/obofoundry/sssom/cli/SimpleCLI.java +++ b/cli/src/main/java/org/incenp/obofoundry/sssom/cli/SimpleCLI.java @@ -263,6 +263,11 @@ boolean isCondensationEnabled() { return pr.hasMatchedOption("condensation") ? enableCondensation : defaultEnableCondensation; } + @Option(names = "--condense-all-slots", + defaultValue = "false", + description = "Condense all slots that can be condensed, even those for which the specification recommends against condensation. Note that `--no-condensation` takes precedence.") + boolean condenseAll; + @Option(names = { "-f", "--output-format" }, paramLabel = "FMT", description = "Write output in the specified format. Allowed values: ${COMPLETION-CANDIDATES}. Default is tsv.", @@ -824,6 +829,7 @@ private SSSOMWriter getWriter(String filename, String metaFilename, Serialisatio writer.setExtraMetadataPolicy(outputOpts.getExtraMetadataPolicy()); writer.setCondensationEnabled(outputOpts.isCondensationEnabled()); + writer.setForceCondensation(outputOpts.condenseAll); writer.setSortingEnabled(outputOpts.sortMappings); writer.setTargetVersion(outputOpts.forceVersion); return writer; diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/JSONWriter.java b/core/src/main/java/org/incenp/obofoundry/sssom/JSONWriter.java index c128ebe..8301d00 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/JSONWriter.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/JSONWriter.java @@ -44,7 +44,6 @@ import org.incenp.obofoundry.sssom.slots.ExtensionSlot; import org.incenp.obofoundry.sssom.slots.Slot; import org.incenp.obofoundry.sssom.slots.SlotHelper; -import org.incenp.obofoundry.sssom.slots.SlotPropagator; import org.incenp.obofoundry.sssom.slots.SlotVisitorBase; import org.incenp.obofoundry.sssom.slots.StringSlot; import org.incenp.obofoundry.sssom.slots.VersionSlot; @@ -132,7 +131,7 @@ public void setWriteCurieMapInContext(boolean enabled) { @Override protected void doWrite(MappingSet mappingSet) throws IOException { // Condense the set - Set condensedSlots = new SlotPropagator(condensationPolicy).condense(mappingSet, true); + Set condensedSlots = condenseSet(mappingSet); // Determine minimum compliant version mappingSet.setSssomVersion(Version.getCompliantVersion(mappingSet)); diff --git a/core/src/main/java/org/incenp/obofoundry/sssom/SSSOMWriter.java b/core/src/main/java/org/incenp/obofoundry/sssom/SSSOMWriter.java index 22344e2..7666c4a 100644 --- a/core/src/main/java/org/incenp/obofoundry/sssom/SSSOMWriter.java +++ b/core/src/main/java/org/incenp/obofoundry/sssom/SSSOMWriter.java @@ -47,6 +47,7 @@ public abstract class SSSOMWriter { protected Version targetVersion = Version.LATEST; protected ExtensionSlotManager extensionManager; protected PrefixManager prefixManager = new PrefixManager(); + protected boolean forceCondensation = false; private boolean customMap = false; private boolean sortMappings = true; @@ -85,6 +86,27 @@ public void setCondensationEnabled(boolean enabled) { condensationPolicy = enabled ? PropagationPolicy.NeverReplace : PropagationPolicy.Disabled; } + /** + * Enables or disables the condensation of “propagatable slots” that the SSSOM + * specification recommends not to condense. + *

+ * The general rule about condensation is that all propagatable slots are + * condensable and should be condensed, but some slots are an exception in that, + * while they may be condensed, they generally should not be. The default + * behaviour of SSSOM writers is therefore not to condense such slots. Use this + * method to change that behaviour. + *

+ * Note that {@link #setCondensationEnabled(boolean)} takes precedence over this + * method. That is, if condensation is disabled, then calling + * setForceCondensation(true) has no effect. + * + * @param enabled If true, forces the writer to condense slots that + * are better left non-condensed. + */ + public void setForceCondensation(boolean enabled) { + forceCondensation = enabled; + } + /** * Sets the highest version of the SSSOM specification that the written set must * be compliant with. @@ -202,7 +224,7 @@ protected Set getUsedPrefixes(MappingSet mappingSet, boolean includeBuil * (may be empty if no slots have been condensed at all). */ protected Set condenseSet(MappingSet mappingSet) { - return new SlotPropagator(condensationPolicy, targetVersion).condense(mappingSet, true); + return new SlotPropagator(condensationPolicy, targetVersion, forceCondensation).condense(mappingSet, true); } /* diff --git a/core/src/test/java/org/incenp/obofoundry/sssom/JSONWriterTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/JSONWriterTest.java index ceec573..93b238a 100644 --- a/core/src/test/java/org/incenp/obofoundry/sssom/JSONWriterTest.java +++ b/core/src/test/java/org/incenp/obofoundry/sssom/JSONWriterTest.java @@ -92,6 +92,12 @@ void testEscapingJSON() throws IOException, SSSOMFormatException { assertWrittenAsExpected(ms, "test-escaping-json", null, null); } + @Test + void testCondenseCoreSlots() throws IOException { + MappingSet ms = getTestSet(); + assertWrittenAsExpected(ms, "test-condensed-core-slots", null, (w) -> w.setForceCondensation(true)); + } + @Test void testBasicRoundtrip() throws IOException, SSSOMFormatException { JSONReader reader = new JSONReader("src/test/resources/sets/exo2c.sssom.json"); diff --git a/core/src/test/java/org/incenp/obofoundry/sssom/TSVReaderTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/TSVReaderTest.java index 47eb741..2bad07a 100644 --- a/core/src/test/java/org/incenp/obofoundry/sssom/TSVReaderTest.java +++ b/core/src/test/java/org/incenp/obofoundry/sssom/TSVReaderTest.java @@ -830,6 +830,21 @@ void testAcceptingLaxEnumValues() throws IOException { } } + @Test + void testCoreSlotsCondensed() throws IOException { + TSVReader reader = new TSVReader("src/test/resources/sets/exo2c-core-slots-condensed.sssom.tsv"); + MappingSet ms; + + try { + ms = reader.read(); + Assertions.assertNull(ms.getMappingJustification()); + Assertions.assertEquals("https://w3id.org/semapv/vocab/ManualMappingCuration", + ms.getMappings().get(0).getMappingJustification()); + } catch ( SSSOMFormatException e ) { + Assertions.fail(e); + } + } + private void compare(ExtensionDefinition expected, ExtensionDefinition actual) { Assertions.assertEquals(expected.getSlotName(), actual.getSlotName()); Assertions.assertEquals(expected.getProperty(), actual.getProperty()); diff --git a/core/src/test/java/org/incenp/obofoundry/sssom/TSVWriterTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/TSVWriterTest.java index f1fa313..d9fee06 100644 --- a/core/src/test/java/org/incenp/obofoundry/sssom/TSVWriterTest.java +++ b/core/src/test/java/org/incenp/obofoundry/sssom/TSVWriterTest.java @@ -439,6 +439,23 @@ void testWriteForVersion() throws IOException { } } + @Test + void testWriteCondensedCoreSlots() throws IOException { + MappingSet ms = getTestSet(); + + File written = new File("src/test/resources/output/test-condensed-core-slots.sssom.tsv.out"); + TSVWriter writer = new TSVWriter(written); + writer.setForceCondensation(true); + writer.write(ms); + + File expected = new File("src/test/resources/output/test-condensed-core-slots.sssom.tsv"); + boolean same = FileUtils.contentEquals(expected, written); + Assertions.assertTrue(same); + if ( same ) { + written.delete(); + } + } + /* * Checks that a mapping set is written exactly as we expect. This method will * write the provided set to a temporary file and compares the written file with diff --git a/core/src/test/resources/output/test-condensed-core-slots.sssom.json b/core/src/test/resources/output/test-condensed-core-slots.sssom.json new file mode 100644 index 0000000..d6b524d --- /dev/null +++ b/core/src/test/resources/output/test-condensed-core-slots.sssom.json @@ -0,0 +1,27 @@ +{ + "sssom_version": "1.1", + "mapping_set_id": "https://example.org/sets/exo2c", + "creator_id": [ + "https://example.org/people/0000-0000-0001-1234", + "https://example.com/people/0000-0000-0002-5678" + ], + "license": "https://creativecommons.org/licenses/by/4.0/", + "subject_type": "owl class", + "predicate_id": "http://www.w3.org/2004/02/skos#closeMatch", + "mapping_justification": "https://w3id.org/semapv/vocab/ManualMappingCuration", + "mappings": [ + { + "subject_id": "https://example.org/entities/0001", + "subject_label": "alice", + "object_id": "https://example.com/entities/0011", + "object_label": "alpha" + }, + { + "subject_id": "https://example.org/entities/0002", + "subject_label": "bob", + "object_id": "https://example.com/entities/0012", + "object_label": "beta", + "confidence": 0.7 + } + ] +} diff --git a/core/src/test/resources/output/test-condensed-core-slots.sssom.tsv b/core/src/test/resources/output/test-condensed-core-slots.sssom.tsv new file mode 100644 index 0000000..5157fbf --- /dev/null +++ b/core/src/test/resources/output/test-condensed-core-slots.sssom.tsv @@ -0,0 +1,10 @@ +#sssom_version: "1.1" +#curie_map: +# COMENT: https://example.com/entities/ +# ORGENT: https://example.org/entities/ +#mapping_set_id: https://example.org/sets/exo2c +#license: https://creativecommons.org/licenses/by/4.0/ +#predicate_id: skos:closeMatch +#mapping_justification: semapv:ManualMappingCuration +subject_id subject_label object_id object_label +ORGENT:0001 alice COMENT:0011 alpha diff --git a/core/src/test/resources/sets/exo2c-core-slots-condensed.sssom.tsv b/core/src/test/resources/sets/exo2c-core-slots-condensed.sssom.tsv new file mode 100644 index 0000000..40fe8a1 --- /dev/null +++ b/core/src/test/resources/sets/exo2c-core-slots-condensed.sssom.tsv @@ -0,0 +1,23 @@ +#sssom_version: "1.1" +#curie_map: +# COMENT: https://example.com/entities/ +# COMPID: https://example.com/people/ +# ORGENT: https://example.org/entities/ +# ORGPID: https://example.org/people/ +#mapping_set_id: https://example.org/sets/exo2c +#mapping_set_title: O2C set +#creator_id: +# - ORGPID:0000-0000-0001-1234 +# - COMPID:0000-0000-0002-5678 +#license: https://creativecommons.org/licenses/by/4.0/ +#mapping_justification: semapv:ManualMappingCuration +#publication_date: 2023-09-13 +subject_id subject_label predicate_id object_id object_label +ORGENT:0001 alice skos:closeMatch COMENT:0011 alpha +ORGENT:0002 bob skos:closeMatch COMENT:0012 beta +ORGENT:0004 daphne skos:closeMatch COMENT:0014 delta +ORGENT:0005 eve skos:closeMatch COMENT:0015 epsilon +ORGENT:0006 fanny skos:closeMatch COMENT:0016 zeta +ORGENT:0007 gavin skos:exactMatch COMENT:0013 gamma +ORGENT:0008 hector skos:closeMatch COMENT:0017 eta +ORGENT:0009 ivan skos:exactMatch COMENT:0019 iota