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 9179b7b9..ee70fb81 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 c128ebe6..8301d005 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 22344e2c..7666c4ae 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/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java b/core/src/main/java/org/incenp/obofoundry/sssom/model/MappingSet.java index cd718c87..134df1ea 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/java/org/incenp/obofoundry/sssom/model/Propagatable.java b/core/src/main/java/org/incenp/obofoundry/sssom/model/Propagatable.java index 1e93d1c8..de60b355 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 31609768..1c4b9dec 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 7932115b..fc04f69e 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()); + } } } } 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 d1949e37..ffe066a0 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 @@ -1496,6 +1528,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 c9d7c5b9..afe95c8c 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", } @@ -66,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. @@ -95,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 2b8bc07e..7ac1e372 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 }}") 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 ceec5735..93b238ad 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 47eb7419..2bad07a0 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 f1fa3132..d9fee065 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/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java b/core/src/test/java/org/incenp/obofoundry/sssom/slots/SlotPropagatorTest.java index d4a865b9..8d1fb324 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 ac281c9f..bcf99521 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()); + } } 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 00000000..d6b524d8 --- /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 00000000..5157fbf9 --- /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 00000000..40fe8a18 --- /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