/** * */ package se.liu.imt.mi.snomedct.parser; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.semanticweb.owlapi.io.AbstractOWLRenderer; import org.semanticweb.owlapi.io.OWLRendererException; import org.semanticweb.owlapi.model.AxiomType; import org.semanticweb.owlapi.model.ClassExpressionType; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLAnnotation; import org.semanticweb.owlapi.model.OWLClass; import org.semanticweb.owlapi.model.OWLClassExpression; import org.semanticweb.owlapi.model.OWLEquivalentClassesAxiom; import org.semanticweb.owlapi.model.OWLLiteral; import org.semanticweb.owlapi.model.OWLLogicalAxiom; import org.semanticweb.owlapi.model.OWLLogicalEntity; import org.semanticweb.owlapi.model.OWLObjectIntersectionOf; import org.semanticweb.owlapi.model.OWLObjectProperty; import org.semanticweb.owlapi.model.OWLObjectSomeValuesFrom; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.OWLSubClassOfAxiom; import org.semanticweb.owlapi.profiles.OWL2ELProfile; import org.semanticweb.owlapi.profiles.OWLProfileReport; import org.semanticweb.owlapi.search.EntitySearcher; import org.semanticweb.owlapi.vocab.OWLRDFVocabulary; /** * @author Daniel Karlsson, Linköping University, daniel.karlsson@liu.se * @author Kent Spackman, IHTSDO, ksp@ihtsdo.org * */ public class SNOMEDCTRenderer extends AbstractOWLRenderer { static Logger logger = Logger.getLogger(SNOMEDCTRenderer.class); static final IRI roleGroupIRI = IRI .create("http://snomed.info/id/609096000"); private boolean labels = true; public SNOMEDCTRenderer() { this(true); } public SNOMEDCTRenderer(boolean labels) { super(); this.labels = labels; } /* * (non-Javadoc) * * @see * org.semanticweb.owlapi.io.AbstractOWLRenderer#render(org.semanticweb. * owlapi.model.OWLOntology, java.io.Writer) */ @Override public void render(OWLOntology ontology, Writer writer) throws OWLRendererException { // check that the ontology is in the OWL 2 EL profile, OWL 2 EL profile // is still more expressive than SNOMED CT Compositional Grammar // commented out to test specific criteria when rendering // TODO: check performance of profile checker // TODO: remove try block? // OWL2ELProfile profile = new OWL2ELProfile(); // OWLProfileReport report = profile.checkOntology(ontology); // if(!report.isInProfile()) // throw new OWLRendererException("Ontology not in OWL 2 EL profile"); try { writeExpressions(ontology, writer); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * Method for writing all SNOMED CT Compositional Grammar compliant * expressions from an ontology. Not all OWL (or OWL 2 EL) expressions can * be translated to Compositional Grammar. The compositional grammar is * extended to allow rendering of SubclassOf axioms. * * @param ontology * @param writer * @throws IOException * Thrown if writing fails. */ private void writeExpressions(OWLOntology ontology, Writer writer) throws IOException { // iterate over all logical axioms, so far only equivalent classes and // subclass axioms are rendered for (OWLLogicalAxiom axiom : ontology.getLogicalAxioms()) { if (axiom.getAxiomType() == AxiomType.EQUIVALENT_CLASSES) { try { writeEquivalentExpression( (OWLEquivalentClassesAxiom) axiom, ontology, writer); } catch (OWLRendererException e) { // TODO Auto-generated catch block e.printStackTrace(); } continue; } if (axiom.getAxiomType() == AxiomType.SUBCLASS_OF) { try { writeSubclassExpression((OWLSubClassOfAxiom) axiom, ontology, writer); } catch (OWLRendererException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } /** * Method for writing a single expression corresponding to an OWL equivalent * classes axiom. * * @param axiom * @param ontology * @param writer * @throws IOException * Thrown if writing fails. * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeEquivalentExpression(OWLEquivalentClassesAxiom axiom, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { logger.debug(axiom.toString()); // create local writer to ensure that content isn't written if the // expression has errors StringWriter localWriter = new StringWriter(); // get the operands of the OWL equivalent classes axiom List<OWLClassExpression> classExpressions = axiom .getClassExpressionsAsList(); if (classExpressions.size() != 2) throw new OWLRendererException( "Number of expressions in equivalence axiom != 2"); // assume that expressions have fixed positions OWLClassExpression expressionClass = classExpressions.get(0); OWLClassExpression expressionDefinition = classExpressions.get(1); // TODO: Should be allowed? if (expressionClass.getClassExpressionType() != ClassExpressionType.OWL_CLASS) throw new OWLRendererException( "Left-hand side not an OWL class in equivalence axiom: " + expressionClass.getClassExpressionType() .toString()); localWriter.write('('); writeEntity((OWLClass) expressionClass, ontology, localWriter); // write "===" for fully defined (or equivalent classes axiom) localWriter.write(")===("); // typically there will be an intersection of classes at the top of the // OWL expression if (expressionDefinition.getClassExpressionType() == ClassExpressionType.OBJECT_INTERSECTION_OF) writeIntersection((OWLObjectIntersectionOf) expressionDefinition, ontology, localWriter); // the exception being when there is exactly one class, i.e. a statement // of equivalence between two named classes, probably not that common else if (expressionDefinition.getClassExpressionType() == ClassExpressionType.OWL_CLASS) writeEntity((OWLClass) expressionDefinition, ontology, localWriter); // other expression types are not allowed in Compositional // Grammar else throw new OWLRendererException( "Non-allowed expression type in equivalence axiom: " + expressionDefinition.getClassExpressionType() .toString()); localWriter.write(')'); // expression is finally written, new line writer.write(localWriter.toString() + '\n'); } /** * Method for writing a single expression corresponding to an OWL subclassof * axiom. * * @param axiom * @param ontology * @param writer * @throws IOException * Thrown if writing fails. * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeSubclassExpression(OWLSubClassOfAxiom axiom, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { logger.debug(axiom.toString()); // create local writer to ensure that content isn't written if the // expression has errors StringWriter localWriter = new StringWriter(); // get the operands of the OWL subclassof axiom OWLClassExpression expressionClass = axiom.getSubClass(); OWLClassExpression expressionDefinition = axiom.getSuperClass(); if (expressionClass.getClassExpressionType() != ClassExpressionType.OWL_CLASS) throw new OWLRendererException( "Left-hand side not an OWL class in subclassof axiom: " + expressionClass.getClassExpressionType() .toString()); localWriter.write('('); writeEntity((OWLClass) expressionClass, ontology, localWriter); // write "<<<" for primitive (or subclassof axiom) localWriter.write(")<<<("); // typically there will be an intersection of classes at the top of the // OWL expression if (expressionDefinition.getClassExpressionType() == ClassExpressionType.OBJECT_INTERSECTION_OF) writeIntersection((OWLObjectIntersectionOf) expressionDefinition, ontology, localWriter); // when there is exactly one class, i.e. a statement // of subclass relationship between two named classes else if (expressionDefinition.getClassExpressionType() == ClassExpressionType.OWL_CLASS) writeEntity((OWLClass) expressionDefinition, ontology, localWriter); // other expression types are not allowed in Compositional // Grammar else throw new OWLRendererException( "Non-allowed expression type in subclassof axiom: " + expressionDefinition.getClassExpressionType() .toString()); localWriter.write(')'); // expression is finally written, new line writer.write(localWriter.toString() + '\n'); } /** * Method for writing an intersection expression as it appears in the root * of an expression definition, or if nested, as a filler of an existential * restriction. * * @param intersection * @param ontology * @param writer * @throws IOException * Thrown when writing fails * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeIntersection(OWLObjectIntersectionOf intersection, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { Set<OWLClassExpression> operands = intersection.getOperands(); Set<OWLClass> genera = new HashSet<OWLClass>(); Set<OWLObjectSomeValuesFrom> differentiae = new HashSet<OWLObjectSomeValuesFrom>(); // extract genera and differentiae from the OWL intersection operand // expressions collectGeneraAndDifferentia(operands, genera, differentiae); // an expression must have at least one genus according to Compositional // Grammar if (genera.isEmpty()) throw new OWLRendererException("No genera in expression"); // write all genera separated by infix '+' boolean first = true; for (OWLClass genus : genera) { if (first) first = false; else writer.write('+'); writeEntity(genus, ontology, writer); } if (!differentiae.isEmpty()) { // separate genera and differentiae with a ':' writer.write(':'); writeDifferentiae(differentiae, ontology, writer); } } /** * Method for writing differentiae. Differentia may be role grouped. * * @param differentiae * @param ontology * @param writer * @throws IOException * Thrown when writing fails * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeDifferentiae(Set<OWLObjectSomeValuesFrom> differentiae, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { final OWLObjectProperty roleGroupProperty = ontology .getOWLOntologyManager().getOWLDataFactory() .getOWLObjectProperty(roleGroupIRI); boolean first = true; for (OWLObjectSomeValuesFrom differentia : differentiae) { if (first) first = false; else writer.write(','); // if it's a role group... if (differentia.getProperty().getNamedProperty() .equals(roleGroupProperty)) { OWLClassExpression roleGroupContent = differentia.getFiller(); // it it's a single differentia in the role group if (roleGroupContent.getClassExpressionType() == ClassExpressionType.OBJECT_SOME_VALUES_FROM) { writer.write('{'); writeSomeValuesFrom( (OWLObjectSomeValuesFrom) roleGroupContent, ontology, writer); writer.write('}'); } // if there is an intersection of differentiae in the role // group... else if (roleGroupContent.getClassExpressionType() == ClassExpressionType.OBJECT_INTERSECTION_OF) { writer.write('{'); OWLObjectIntersectionOf intersection = (OWLObjectIntersectionOf) roleGroupContent; Set<OWLObjectSomeValuesFrom> roleGroupDifferentiae = new HashSet<OWLObjectSomeValuesFrom>(); for (OWLClassExpression operand : intersection .getOperands()) // only existential restrictions are allowed in role // groups, other kinds of expression will cause // exception if (operand.getClassExpressionType() == ClassExpressionType.OBJECT_SOME_VALUES_FROM) roleGroupDifferentiae .add((OWLObjectSomeValuesFrom) operand); else throw new OWLRendererException( "Role group can only directly contain existential restrictions: " + operand.getClassExpressionType() .toString()); writeDifferentiae(roleGroupDifferentiae, ontology, writer); writer.write('}'); } else throw new OWLRendererException( "Role group must contain either a single existential restriction or an intersection: " + roleGroupContent.getClassExpressionType() .toString()); } else { // not a role group... writeSomeValuesFrom((OWLObjectSomeValuesFrom) differentia, ontology, writer); } } } /** * Method for writing existential restriction expressions. The filler of the * expressions could potentially be a nested expression. * * @param differentia * @param ontology * @param writer * @throws IOException * Thrown when writing fails * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeSomeValuesFrom(OWLObjectSomeValuesFrom differentia, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { OWLObjectProperty property = differentia.getProperty() .asOWLObjectProperty(); writeEntity(property, ontology, writer); writer.write('='); OWLClassExpression filler = differentia.getFiller(); logger.debug("filler = " + filler.toString()); // if filler is an intersection, then it is a nested expression if (filler.getClassExpressionType() == ClassExpressionType.OBJECT_INTERSECTION_OF) { writer.write('('); writeIntersection((OWLObjectIntersectionOf) filler, ontology, writer); writer.write(')'); } else if (filler.getClassExpressionType() == ClassExpressionType.OWL_CLASS) writeEntity((OWLClass) filler, ontology, writer); else throw new OWLRendererException( "Type of the filler of an existentional restriction can only be a class or an intersection: " + filler.getClassExpressionType().toString()); } /** * Utility method for extracting genera and differentiae from a set of class * expressions. Genera appear as OWLClass objects, differentiae appear as * existential restrictions. Both could be nested inside intersections * recursively. * * @param operands * @param genera * Set of OWL classes, changed through side effects * @param differentia * Set of OWL existential restrictions, changed through side * effects * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void collectGeneraAndDifferentia(Set<OWLClassExpression> operands, Set<OWLClass> genera, Set<OWLObjectSomeValuesFrom> differentia) throws OWLRendererException { for (OWLClassExpression operand : operands) { switch (operand.getClassExpressionType()) { case OWL_CLASS: genera.add((OWLClass) operand); break; case OBJECT_INTERSECTION_OF: collectGeneraAndDifferentia( ((OWLObjectIntersectionOf) operand).getOperands(), genera, differentia); break; case OBJECT_SOME_VALUES_FROM: differentia.add((OWLObjectSomeValuesFrom) operand); break; default: throw new OWLRendererException("Class expression not allowed: " + operand.getClassExpressionType().toString()); } } } /** * Utility method for writing an OWL entity, here an OWL class or object * property, as a SCTID plus label. * * @param entity * @param ontology * @param writer * @throws IOException * Thrown when writing fails * @throws OWLRendererException * Thrown if the OWL expression cannot be rendered as a * Compositional Grammar statement. */ private void writeEntity(OWLLogicalEntity entity, OWLOntology ontology, Writer writer) throws IOException, OWLRendererException { String sctid = extractID(entity.getIRI().toString()); logger.debug(sctid); writer.write(sctid); if (labels) { // only rdfs:label annotations will be written String label = getLabel(entity, ontology); if (label != null) { writer.write("|" + label + "|"); } } } /** * Utility method for getting a rdfs:label from an OWL entity. * * @param entity * @param ontology * @return The label as a String. */ private String getLabel(OWLLogicalEntity entity, OWLOntology ontology) { String label = null; Collection<OWLAnnotation> annotations = EntitySearcher.getAnnotations( entity.getIRI(), ontology); for (OWLAnnotation annotation : annotations) { if (annotation.getProperty().getIRI() .equals(OWLRDFVocabulary.RDFS_LABEL.getIRI())) { OWLLiteral val = (OWLLiteral) annotation.getValue(); label = val.getLiteral(); } } return label; } /** * Utility method for extracting SCTID from an IRI * * @param iri * @return The substring after the last '/' in the IRI, the SCTID if the IRI * is a SNOMED CT URI * @throws OWLRendererException * Thrown if the IRI ends with '/' and thus cannot be a valid * SNOMED CT URI */ String extractID(String iri) throws OWLRendererException { if (iri.endsWith("/")) throw new OWLRendererException("IRI ends with '/'"); else { String sctid = iri.substring(iri.lastIndexOf('/') + 1); // if (!isPositiveInteger(sctid)) // throw new OWLRendererException( // "SCTID part of IRI is non-numeric"); // else return sctid; } } public static boolean isPositiveInteger(String str) { for (char c : str.toCharArray()) if (!Character.isDigit(c)) return false; return true; } }