From 21abbf09ebe0b1f78d342b8ff45197a5660992a7 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Sun, 10 Mar 2024 21:33:28 +0000 Subject: [PATCH] Lax comparison of language tags (WIP). As discussed in https://github.com/INCATools/kgcl/issues/60, when trying to find an annotation value (e.g. to rename a class, we need to find the annotation corresponding to the old label), language tags should be compared in a relaxed fashion. We should not fail to find an annotation just because the annotation has a language tag and the KGCL command did not specify any language tag at all. This necessitates some pretty important refactoring, because this means, among other things, that more than one annotation values may match (if several annotations have the same literal value but different language tags, or one has a language tag and another does not). This is still a work in progress. For now, this is implemented specifically for the NodeRename operation. After more testing, this will be generalized to all other operations that involve finding an existing annotation value (e.g. RemoveDefinition, ChangeDefinition, SynonymReplacement, etc.). --- .../obofoundry/kgcl/DirectOWLTranslator.java | 96 ++++++++++++++++--- .../kgcl/DirectOWLTranslatorTest.java | 76 +++++++++++++++ 2 files changed, 157 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/org/incenp/obofoundry/kgcl/DirectOWLTranslator.java b/core/src/main/java/org/incenp/obofoundry/kgcl/DirectOWLTranslator.java index 33148ce..ca5d887 100644 --- a/core/src/main/java/org/incenp/obofoundry/kgcl/DirectOWLTranslator.java +++ b/core/src/main/java/org/incenp/obofoundry/kgcl/DirectOWLTranslator.java @@ -137,12 +137,80 @@ private boolean compareValue(OWLAnnotationValue value, String changeText, String return valueLang.equals(changeLang); } + /* + * Helper method to find the annotations whose value matches what a change + * expects. + */ + private Set findMatchingAnnotations(IRI property, IRI entity, String text, String lang, + String datatype, String newLang) { + HashSet axioms = new HashSet(); + OWLAnnotationAssertionAxiom langLessAxiom = null; + for ( OWLAnnotationAssertionAxiom ax : ontology.getAnnotationAssertionAxioms(entity) ) { + if ( !ax.getProperty().getIRI().equals(property) ) { + continue; + } + + OWLAnnotationValue value = ax.getValue(); + if ( !value.isLiteral() ) { + continue; + } + + String valueText = value.asLiteral().get().getLiteral(); + if ( !valueText.equals(text) ) { + continue; + } + + String valueLang = value.asLiteral().get().getLang(); + if ( valueLang.isEmpty() ) { + // We'll decide later what to do with this one + langLessAxiom = ax; + continue; + } + + // If we are expecting a given language, the language of the value must match. + if ( lang != null && !valueLang.equals(lang) ) { + continue; + } + + // If the new value has an explicit language tag, then even if no language tag + // has been explicitly specified for the old value, we can only accept a value + // with the same language as the new value. + if ( newLang != null && !valueLang.equals(newLang) ) { + continue; + } + + // At this point both the text and the language match. + axioms.add(ax); + } + + if ( langLessAxiom != null ) { + // We accept the langless axiom only if: + // - no language tag was explicitly specified on the old value + // - if alanguage tag was explicitly specified on the new value, we didn't find + // any annotation in that language + // - if a datatype was explicitly specified, it matches the datatype of the + // langless axiom's value + if ( lang == null && (newLang == null || axioms.isEmpty()) && (datatype == null || langLessAxiom.getValue() + .asLiteral().get().getDatatype().getIRI().toString().equals(datatype)) ) { + axioms.add(langLessAxiom); + } + } + + return axioms; + } + private OWLLiteral getLiteral(NodeChange change) { + return getLiteral(change, null); + } + + private OWLLiteral getLiteral(NodeChange change, String oldLang) { if ( change.getNewLanguage() != null ) { return factory.getOWLLiteral(change.getNewValue(), change.getNewLanguage()); } else if ( change.getNewDatatype() != null ) { return factory.getOWLLiteral(change.getNewValue(), factory.getOWLDatatype(IRI.create(change.getNewDatatype()))); + } else if ( oldLang != null ) { + return factory.getOWLLiteral(change.getNewValue(), oldLang); } else { return factory.getOWLLiteral(change.getNewValue()); } @@ -261,27 +329,25 @@ public List visit(NodeRename v) { return empty; } - OWLAnnotationAssertionAxiom oldLabelAxiom = null; - for ( OWLAnnotationAssertionAxiom ax : ontology - .getAnnotationAssertionAxioms(IRI.create(v.getAboutNode().getId())) ) { - if ( ax.getProperty().getIRI().equals(OWLRDFVocabulary.RDFS_LABEL.getIRI()) ) { - if ( compareValue(ax.getValue(), v.getOldValue(), v.getOldLanguage()) ) { - oldLabelAxiom = ax; - } - } - } + IRI nodeIRI = IRI.create(v.getAboutNode().getId()); + Set matches = findMatchingAnnotations(OWLRDFVocabulary.RDFS_LABEL.getIRI(), + nodeIRI, v.getOldValue(), v.getOldLanguage(), v.getOldDatatype(), v.getNewLanguage()); - if ( oldLabelAxiom == null ) { + if ( matches.isEmpty() ) { onReject(v, "Label \"%s\" not found on <%s>", v.getOldValue(), v.getAboutNode().getId()); return empty; } - AddAxiom addNewLabel = new AddAxiom(ontology, - factory.getOWLAnnotationAssertionAxiom( - factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_LABEL.getIRI()), - IRI.create(v.getAboutNode().getId()), getLiteral(v))); + ArrayList changes = new ArrayList(); + for ( OWLAnnotationAssertionAxiom match : matches ) { + changes.add(removeAxiom(match)); + changes.add(new AddAxiom(ontology, + factory.getOWLAnnotationAssertionAxiom( + factory.getOWLAnnotationProperty(OWLRDFVocabulary.RDFS_LABEL.getIRI()), nodeIRI, + getLiteral(v, match.getValue().asLiteral().get().getLang())))); + } - return makeList(removeAxiom(oldLabelAxiom), addNewLabel); + return changes; } @Override diff --git a/core/src/test/java/org/incenp/obofoundry/kgcl/DirectOWLTranslatorTest.java b/core/src/test/java/org/incenp/obofoundry/kgcl/DirectOWLTranslatorTest.java index 9e0373a..91c664d 100644 --- a/core/src/test/java/org/incenp/obofoundry/kgcl/DirectOWLTranslatorTest.java +++ b/core/src/test/java/org/incenp/obofoundry/kgcl/DirectOWLTranslatorTest.java @@ -127,6 +127,82 @@ void testNodeRename() { testChange(change, expected, null); } + @Test + void testNodeRenameNoLangTags() { + NodeRename change = new NodeRename(); + setAboutNode(change, "LaReine"); + setValue(change, "LaReine", null, true); + setValue(change, "TheQueen", null); + + // LaReine has two identical labels in both English and Portuguese; since we + // have not specified any language tags, both should be renamed and the original + // language tags should be preserved. + + ArrayList expected = new ArrayList(); + expected.add(new RemoveAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "LaReine", "en"))); + expected.add(new RemoveAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "LaReine", "pt"))); + expected.add(new AddAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "TheQueen", "en"))); + expected.add(new AddAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "TheQueen", "pt"))); + + testChange(change, expected, null); + } + + @Test + void testNodeRenameNoOldLangTag() { + NodeRename change = new NodeRename(); + setAboutNode(change, "LaReine"); + setValue(change, "LaReine", null, true); + setValue(change, "TheQueen", "en"); + + // Here, since we have specified English as the NEW language tag, only the + // original English label should be renamed; the Portuguese one should be left + // untouched, even if its literal value is the same as the English label. + + ArrayList expected = new ArrayList(); + expected.add(new RemoveAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "LaReine", "en"))); + expected.add(new AddAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "TheQueen", "en"))); + + testChange(change, expected, null); + } + + @Test + void testNodeRenameNoNewLangTag() { + NodeRename change = new NodeRename(); + setAboutNode(change, "LaReine"); + setValue(change, "LaReine", "en", true); + setValue(change, "TheQueen", null); + + // Only the English label should be modified since we have explicitly specified + // a language tag on the old value; furthermore, even if the new value has no + // language tag, the original tag should be carried over. + + ArrayList expected = new ArrayList(); + expected.add(new RemoveAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "LaReine", "en"))); + expected.add(new AddAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "TheQueen", "en"))); + + testChange(change, expected, null); + } + + @Test + void testNodeRenameExplicitNewDatatype() { + NodeRename change = new NodeRename(); + setAboutNode(change, "LaReine"); + setValue(change, "LaReine", "en", true); + change.setNewValue("TheQueen"); + change.setNewDatatype(XSDVocabulary.STRING.toString()); + + // Only the English label should be modified since we have explicitly specified + // a language tag on the old value; we have also explicitly set a xsd:string + // datatype on the new value, so the old language tag should NOT be carried + // over. + + ArrayList expected = new ArrayList(); + expected.add(new RemoveAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "LaReine", "en"))); + expected.add(new AddAxiom(ontology, getAnnotation(LABEL_IRI, "LaReine", "TheQueen", null))); + + testChange(change, expected, null); + } + @Test void testRejectedNodeRename() { NodeRename change = new NodeRename();