From 8dda5c82142e750b7b306d5805956933d87e3f46 Mon Sep 17 00:00:00 2001 From: Jim Balhoff Date: Sat, 13 Jan 2024 22:13:26 +0000 Subject: [PATCH] Allow arbitrary annotation properties as qualifier tags in OBO #1099 Allowing arbitrary defined annotation properties as qualifier tags. Use oboInOWL as the default namespace when looking up tag IRIs. This helps with backwards compatibility for undeclared annotation properties. Enforce oio namespace for created_by and creation_date. --- .../oboformat/LoadAnonymousTestCase.java | 8 ++- .../oboformat/RoundTripTestBasics.java | 10 ++- .../oboformat/RoundTripTestCase.java | 2 +- .../org/obolibrary/oboformat/TagIRIsTest.java | 69 +++++++++++++++++++ .../BOMSafeInputStreamAndParseTestCase.java | 27 ++++---- contract/src/test/resources/obo/tag_iris.obo | 56 +++++++++++++++ .../ConcurrentOWLOntologyImpl_TestCase.java | 4 +- .../org/obolibrary/obo2owl/OWLAPIObo2Owl.java | 51 +++++++++++--- .../org/obolibrary/obo2owl/OWLAPIOwl2Obo.java | 2 + .../obolibrary/obo2owl/Obo2OWLConstants.java | 3 + 10 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 contract/src/test/java/org/obolibrary/oboformat/TagIRIsTest.java create mode 100644 contract/src/test/resources/obo/tag_iris.obo diff --git a/contract/src/test/java/org/obolibrary/oboformat/LoadAnonymousTestCase.java b/contract/src/test/java/org/obolibrary/oboformat/LoadAnonymousTestCase.java index 997e045fd9..c0d0af2f80 100644 --- a/contract/src/test/java/org/obolibrary/oboformat/LoadAnonymousTestCase.java +++ b/contract/src/test/java/org/obolibrary/oboformat/LoadAnonymousTestCase.java @@ -5,6 +5,7 @@ import static org.semanticweb.owlapi.vocab.OWL2Datatype.XSD_STRING; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.semanticweb.owlapi.api.test.baseclasses.TestBase; @@ -94,7 +95,10 @@ void shouldLoad() { AnnotationAssertion(Annotation(hasDbXref, literal), iao0000115, uo0.getIRI(), literal("A unit of measurement is a standardized quantity of a physical quality.")), AnnotationAssertion(Annotation(hasDbXref, literal), iao0000115, uo1.getIRI(), - literal("A unit which is a standard measure of the distance between two points."))); - assertEquals(expected, asUnorderedSet(ontology.axioms())); + literal("A unit which is a standard measure of the distance between two points.")), + AnnotationAssertion(RDFSLabel(), createdBy.getIRI(), literal("created by")), + AnnotationAssertion(RDFSLabel(), id.getIRI(), literal("id"))); + + assertEquals(expected.stream().map(Object::toString).sorted().collect(Collectors.joining("\n")), ontology.axioms().map(Object::toString).sorted().collect(Collectors.joining("\n"))); } } diff --git a/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestBasics.java b/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestBasics.java index 23508414b5..b71a7cf331 100644 --- a/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestBasics.java +++ b/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestBasics.java @@ -12,6 +12,7 @@ import org.obolibrary.oboformat.diff.OBODocDiffer; import org.obolibrary.oboformat.model.FrameStructureException; import org.obolibrary.oboformat.model.OBODoc; +import org.semanticweb.owlapi.formats.OBODocumentFormat; import org.semanticweb.owlapi.io.StringDocumentTarget; import org.semanticweb.owlapi.model.OWLAxiom; import org.semanticweb.owlapi.model.OWLOntology; @@ -43,14 +44,19 @@ public List roundTripOBODoc(OBODoc obodoc, boolean isExpectRoundtrip) OWLOntology oo = convert(obodoc); StringDocumentTarget oo1 = saveOntology(oo); OBODoc obodoc2 = convert(oo); - StringDocumentTarget oo2 = saveOntology(convert(obodoc2)); + OWLOntology o2 = convert(obodoc2); + StringDocumentTarget oo2 = saveOntology(o2); String s1 = oo1.toString(); String s2 = oo2.toString(); assertEquals(s1, s2); obodoc2.check(); List diffs = OBODocDiffer.getDiffs(obodoc, obodoc2); if (isExpectRoundtrip) { - assertEquals(0, diffs.size(), "Expected no diffs but " + diffs); + if (diffs.size() > 0) { + StringDocumentTarget out1 = saveOntology(oo, new OBODocumentFormat()); + StringDocumentTarget out2 = saveOntology(o2, new OBODocumentFormat()); + assertEquals(out1.toString(), out2.toString()); + } } return diffs; } diff --git a/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestCase.java b/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestCase.java index 5e13eedda1..330e8d82b7 100644 --- a/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestCase.java +++ b/contract/src/test/java/org/obolibrary/oboformat/RoundTripTestCase.java @@ -229,7 +229,7 @@ void testRoundTripOWLRO() { Collection values = clause.getQualifierValues(); assertEquals(1, values.size()); QualifierValue value = values.iterator().next(); - assertEquals("http://purl.obolibrary.org/obo/IAO_0000116", value.getQualifier()); + assertEquals("IAO:0000116", value.getQualifier()); assertEquals("From Allen terminology", value.getValue()); found = true; } diff --git a/contract/src/test/java/org/obolibrary/oboformat/TagIRIsTest.java b/contract/src/test/java/org/obolibrary/oboformat/TagIRIsTest.java new file mode 100644 index 0000000000..a6e01aa218 --- /dev/null +++ b/contract/src/test/java/org/obolibrary/oboformat/TagIRIsTest.java @@ -0,0 +1,69 @@ +package org.obolibrary.oboformat; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.semanticweb.owlapi.api.test.baseclasses.TestBase; +import org.semanticweb.owlapi.model.IRI; +import org.semanticweb.owlapi.model.OWLAnnotation; +import org.semanticweb.owlapi.model.OWLAnnotationProperty; +import org.semanticweb.owlapi.model.OWLAxiom; +import org.semanticweb.owlapi.model.OWLClass; +import org.semanticweb.owlapi.model.OWLLiteral; +import org.semanticweb.owlapi.model.OWLOntology; +import org.semanticweb.owlapi.vocab.OWL2Datatype; + +class TagIRIsTest extends TestBase { + + @Test + void testTagIRIMapping() { + OWLAnnotationProperty definition = + df.getOWLAnnotationProperty(IRI.create("http://purl.obolibrary.org/obo/IAO_0000115")); + OWLAnnotationProperty oioCreatedBy = df.getOWLAnnotationProperty( + IRI.create("http://www.geneontology.org/formats/oboInOwl#created_by")); + OWLAnnotationProperty oioInventedBy = df.getOWLAnnotationProperty( + IRI.create("http://www.geneontology.org/formats/oboInOwl#invented_by")); + OWLAnnotationProperty source = + df.getOWLAnnotationProperty(IRI.create("http://purl.obolibrary.org/obo/MYONT_20")); + OWLOntology ont = load("obo/tag_iris.obo", m); + Set axioms = ont.getAxioms(); + OWLClass term1 = df.getOWLClass(IRI.create("http://purl.obolibrary.org/obo/MYONT_1")); + OWLClass term2 = df.getOWLClass(IRI.create("http://purl.obolibrary.org/obo/MYONT_2")); + OWLClass term3 = df.getOWLClass(IRI.create("http://purl.obolibrary.org/obo/MYONT_3")); + OWLClass term4 = df.getOWLClass(IRI.create("http://purl.obolibrary.org/obo/MYONT_4")); + Set annotations = Stream + .of(df.getOWLAnnotation(df.getRDFSComment(), string("Here is a sub-annotation.")), + df.getOWLAnnotation(df.getRDFSSeeAlso(), string("A nested see also value."))) + .collect(Collectors.toSet()); + assertTrue(axioms.contains(df.getOWLAnnotationAssertionAxiom(definition, term1.getIRI(), + string("Definition of term one."), annotations))); + assertTrue(axioms.contains(df.getOWLAnnotationAssertionAxiom(df.getRDFSSeeAlso(), + term1.getIRI(), string("See also value.")))); + assertTrue(axioms.contains(df.getOWLAnnotationAssertionAxiom(definition, term2.getIRI(), + string("Definition of term two."), Collections + .singleton(df.getOWLAnnotation(source, string("A nested annotation value.")))))); + assertTrue(axioms.contains(df.getOWLAnnotationAssertionAxiom(definition, term3.getIRI(), + string("Definition of term three."), Collections + .singleton(df.getOWLAnnotation(source, string("A definition source value.")))))); + assertTrue( + axioms.contains( + df.getOWLAnnotationAssertionAxiom(oioCreatedBy, term3.getIRI(), string("goc:bro"))), + "created_by is built in and should not be overridden by a typedef"); + assertTrue( + axioms.contains(df.getOWLAnnotationAssertionAxiom(definition, term4.getIRI(), + string("Definition of term four."), + Collections + .singleton(df.getOWLAnnotation(oioInventedBy, string("An inventor value."))))), + "An undeclared tag should have oio namespace"); + + } + + protected OWLLiteral string(String l) { + return df.getOWLLiteral(l, OWL2Datatype.XSD_STRING); + } +} diff --git a/contract/src/test/java/org/semanticweb/owlapi/api/test/syntax/rdf/BOMSafeInputStreamAndParseTestCase.java b/contract/src/test/java/org/semanticweb/owlapi/api/test/syntax/rdf/BOMSafeInputStreamAndParseTestCase.java index 12d3a4d6d5..d3f5c9a21a 100644 --- a/contract/src/test/java/org/semanticweb/owlapi/api/test/syntax/rdf/BOMSafeInputStreamAndParseTestCase.java +++ b/contract/src/test/java/org/semanticweb/owlapi/api/test/syntax/rdf/BOMSafeInputStreamAndParseTestCase.java @@ -6,6 +6,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -13,8 +14,10 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.semanticweb.owlapi.api.test.baseclasses.TestBase; +import org.semanticweb.owlapi.formats.*; import org.semanticweb.owlapi.io.ReaderDocumentSource; import org.semanticweb.owlapi.model.IRI; +import org.semanticweb.owlapi.model.OWLDocumentFormat; import org.semanticweb.owlapi.model.OWLOntologyCreationException; class BOMSafeInputStreamAndParseTestCase extends TestBase { @@ -39,13 +42,20 @@ static Collection data() { "" //@formatter:on ); + List formats = Arrays.asList(new OWLXMLDocumentFormat(), + new ManchesterSyntaxDocumentFormat(), + new FunctionalSyntaxDocumentFormat(), + new RioTurtleDocumentFormat(), + new RDFXMLDocumentFormat()); + List prefixes = l(new int[] {0x00, 0x00, 0xFE, 0xFF}, new int[] {0xFF, 0xFE, 0x00, 0x00}, new int[] {0xFF, 0xFE}, new int[] {0xFE, 0xFF}, new int[] {0xEF, 0xBB, 0xBF}); for (int[] p : prefixes) { - for (String onto : list) { - toReturn.add(Arguments.of(p, onto)); - } + for (int i = 0; i < list.size(); i++) { + toReturn.add(Arguments.of(p, list.get(i), formats.get(i))); + + } } return toReturn; } @@ -72,18 +82,9 @@ private static InputStream in(int[] b, String content) throws IOException { // EF BB BF |UTF-8 @ParameterizedTest @MethodSource("data") - void testBOMError32big(int[] b, String input) throws IOException { + void testBOMError32big(int[] b, String input, OWLDocumentFormat format) throws IOException { try (InputStream in = in(b, input)) { loadFrom(in); } } - - @ParameterizedTest - @MethodSource("data") - void testBOMError32bigReader(int[] b, String input) - throws OWLOntologyCreationException, IOException { - try (InputStream in = in(b, input); InputStreamReader r = new InputStreamReader(in)) { - m.loadOntologyFromOntologyDocument(new ReaderDocumentSource(r)); - } - } } diff --git a/contract/src/test/resources/obo/tag_iris.obo b/contract/src/test/resources/obo/tag_iris.obo new file mode 100644 index 0000000000..9e463e4faa --- /dev/null +++ b/contract/src/test/resources/obo/tag_iris.obo @@ -0,0 +1,56 @@ +format-version: 1.2 +ontology: myont + +[Term] +id: MYONT:1 +name: term one +def: "Definition of term one." [] {comment="Here is a sub-annotation.", seeAlso="A nested see also value."} +property_value: seeAlso "See also value." xsd:string + +[Term] +id: MYONT:2 +name: term two +def: "Definition of term two." [] {MYONT:20="A nested annotation value."} +property_value: MYONT:21 "A top level annotation value." xsd:string + +[Term] +id: MYONT:3 +name: term three +def: "Definition of term three." [] {source="A definition source value."} +intersection_of: MYONT:2 ! term two +intersection_of: results_in_transport_across GO:0005739 ! mitochondrion +created_by: goc:bro + +[Term] +id: MYONT:4 +name: term four +def: "Definition of term four." [] {invented_by="An inventor value."} + +[Typedef] +id: source +name: source +xref: MYONT:20 +is_metadata_tag: true + +[Typedef] +id: MYONT:21 +name: source2 +is_metadata_tag: true + +[Typedef] +id: seeAlso +name: see also +xref: http://www.w3.org/2000/01/rdf-schema#seeAlso +is_metadata_tag: true + +[Typedef] +id: results_in_transport_across +name: results in transport across +namespace: external +xref: RO:0002342 + +[Typedef] +id: created_by +name: created by +namespace: external +xref: http://purl.org/dc/terms/creator diff --git a/impl/src/test/java/uk/ac/manchester/cs/owl/owlapi/concurrent/ConcurrentOWLOntologyImpl_TestCase.java b/impl/src/test/java/uk/ac/manchester/cs/owl/owlapi/concurrent/ConcurrentOWLOntologyImpl_TestCase.java index 69292cc41f..cdaced137c 100644 --- a/impl/src/test/java/uk/ac/manchester/cs/owl/owlapi/concurrent/ConcurrentOWLOntologyImpl_TestCase.java +++ b/impl/src/test/java/uk/ac/manchester/cs/owl/owlapi/concurrent/ConcurrentOWLOntologyImpl_TestCase.java @@ -1,6 +1,6 @@ package uk.ac.manchester.cs.owl.owlapi.concurrent; -import static org.mockito.ArgumentMatchers.anyListOf; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -84,7 +84,7 @@ class ConcurrentOWLOntologyImpl_TestCase { void setUp() { when(readWriteLock.readLock()).thenReturn(readLock); when(readWriteLock.writeLock()).thenReturn(writeLock); - when(delegate.applyChangesAndGetDetails(anyListOf(OWLOntologyChange.class))) + when(delegate.applyChangesAndGetDetails(anyList())) .thenReturn(new ChangeDetails(ChangeApplied.NO_OPERATION, Collections.emptyList())); ontology = spy(new ConcurrentOWLOntologyImpl(delegate, readWriteLock)); } diff --git a/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIObo2Owl.java b/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIObo2Owl.java index 8db29ac5b7..3501203aed 100644 --- a/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIObo2Owl.java +++ b/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIObo2Owl.java @@ -1,6 +1,7 @@ package org.obolibrary.obo2owl; import static org.obolibrary.obo2owl.Obo2OWLConstants.DEFAULT_IRI_PREFIX; +import static org.obolibrary.obo2owl.Obo2OWLConstants.OIOVOCAB_IRI_PREFIX; import static org.semanticweb.owlapi.util.OWLAPIPreconditions.checkNotNull; import static org.semanticweb.owlapi.util.OWLAPIPreconditions.emptyOptional; import static org.semanticweb.owlapi.util.OWLAPIPreconditions.optional; @@ -1373,21 +1374,29 @@ protected OWLIndividual trIndividual(String instId) { return fac.getOWLNamedIndividual(iri); } + private IRI trTagToIRIIncludingTypedefs(String tag) { + IRI iri = ANNOTATIONPROPERTYMAP.get(tag); + if (iri == null) { + iri = oboIdToIRI(tag, true); + } + return iri; + } + /** * Translate tag to annotation prop. - * + * * @param tag the tag * @return the OWL annotation property */ protected OWLAnnotationProperty trTagToAnnotationProp(String tag) { - IRI iri = trTagToIRI(tag); + IRI iri = trTagToIRIIncludingTypedefs(tag); OWLAnnotationProperty ap = fac.getOWLAnnotationProperty(iri); if (!apToDeclare.contains(ap)) { apToDeclare.add(ap); add(fac.getOWLDeclarationAxiom(ap)); Obo2OWLVocabulary vocab = Obo2OWLConstants.getVocabularyObj(tag); if (vocab != null) { - add(fac.getOWLAnnotationAssertionAxiom(iri, fac.getRDFSLabel(vocab.getLabel()))); + add(fac.getOWLAnnotationAssertionAxiom(iri, fac.getRDFSLabel(trLiteral(vocab.getLabel())))); } } return ap; @@ -1469,9 +1478,19 @@ protected OWLAnnotationValue trLiteral(Object inputValue) { * @return the iri */ public IRI oboIdToIRI(String id) { + return oboIdToIRI(id, false); + } + + /** + * Obo id to iri. + * + * @param id the id + * @return the iri + */ + public IRI oboIdToIRI(String id, boolean oboInOwlDefault) { IRI iri = idToIRICache.get(id); if (iri == null) { - iri = loadOboToIRI(id); + iri = loadOboToIRI(id, oboInOwlDefault); idToIRICache.put(id, iri); } return iri; @@ -1484,6 +1503,16 @@ public IRI oboIdToIRI(String id) { * @return the iri */ public IRI loadOboToIRI(String id) { + return loadOboToIRI(id, false); + } + + /** + * Obo id to iri. + * + * @param id the id + * @return the iri + */ + public IRI loadOboToIRI(String id, boolean oboInOwlDefault) { if (id.contains(" ")) { LOG.error("id contains space: \"{}\"", id); throw new OWLParserException("spaces not allowed: '" + id + '\''); @@ -1547,9 +1576,14 @@ public IRI loadOboToIRI(String id) { db = getDefaultIDSpace() + '#'; localId = idParts[0]; } - String uriPrefix = DEFAULT_IRI_PREFIX + db; - if (idSpaceMap.containsKey(db)) { - uriPrefix = idSpaceMap.get(db); + String uriPrefix; + if (oboInOwlDefault) { + uriPrefix = OIOVOCAB_IRI_PREFIX; + } else { + uriPrefix = DEFAULT_IRI_PREFIX + db; + if (idSpaceMap.containsKey(db)) { + uriPrefix = idSpaceMap.get(db); + } } String safeId; try { @@ -1570,10 +1604,9 @@ public IRI loadOboToIRI(String id) { } // 5.9.3. Special Rules for Relations - /** * Translate shorthand id to expanded id. - * + * * @param id the id * @return the string */ diff --git a/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIOwl2Obo.java b/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIOwl2Obo.java index fcac3ee467..4988d86541 100644 --- a/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIOwl2Obo.java +++ b/oboformat/src/main/java/org/obolibrary/obo2owl/OWLAPIOwl2Obo.java @@ -479,6 +479,8 @@ public static String owlObjectToTag(OWLObject obj) { String prefix = Obo2OWLConstants.OIOVOCAB_IRI_PREFIX; if (iri.startsWith(prefix)) { tag = iri.substring(prefix.length()); + } else { + tag = getIdentifier(iriObj); } } return tag; diff --git a/oboformat/src/main/java/org/obolibrary/obo2owl/Obo2OWLConstants.java b/oboformat/src/main/java/org/obolibrary/obo2owl/Obo2OWLConstants.java index 93746aa2af..04507cc53c 100644 --- a/oboformat/src/main/java/org/obolibrary/obo2owl/Obo2OWLConstants.java +++ b/oboformat/src/main/java/org/obolibrary/obo2owl/Obo2OWLConstants.java @@ -52,6 +52,9 @@ public enum Obo2OWLVocabulary implements HasIRI { /**IRI_IAO_0100001. */ IRI_IAO_0100001(DEFAULT_IRI_PREFIX, "IAO_0100001", "term replaced by", OboFormatTag.TAG_REPLACED_BY.getTag()), /**IRI_OIO_shorthand. */ IRI_OIO_shorthand(OIOVOCAB_IRI_PREFIX, "shorthand", "shorthand", "shorthand"), /**IRI_OIO_consider. */ IRI_OIO_consider(OIOVOCAB_IRI_PREFIX, "consider", "consider", OboFormatTag.TAG_CONSIDER.getTag()), + /**IRI_OIO_id*/ IRI_OIO_id(OIOVOCAB_IRI_PREFIX, "id", "id", OboFormatTag.TAG_ID.getTag()), + /**IRI_OIO_created_by*/ IRI_OIO_created_by(OIOVOCAB_IRI_PREFIX, "created_by", "created by", OboFormatTag.TAG_CREATED_BY.getTag()), + /**IRI_OIO_creation_date*/ IRI_OIO_creation_date(OIOVOCAB_IRI_PREFIX, "creation_date", "creation date", OboFormatTag.TAG_CREATION_DATE.getTag()), /**IRI_OIO_hasOBOFormatVersion. */ IRI_OIO_hasOBOFormatVersion(OIOVOCAB_IRI_PREFIX, "hasOBOFormatVersion", "has_obo_format_version", OboFormatTag.TAG_FORMAT_VERSION.getTag()), /**IRI_OIO_treatXrefsAsIsA. */ IRI_OIO_treatXrefsAsIsA(OIOVOCAB_IRI_PREFIX, "treat-xrefs-as-is_a", "treat-xrefs-as-is_a", OboFormatTag.TAG_TREAT_XREFS_AS_IS_A.getTag()), /**IRI_OIO_treatXrefsAsHasSubClass. */ IRI_OIO_treatXrefsAsHasSubClass(OIOVOCAB_IRI_PREFIX, "treat-xrefs-as-has-subclass", "treat-xrefs-as-has-subclass", OboFormatTag.TAG_TREAT_XREFS_AS_HAS_SUBCLASS.getTag()),