package uk.ac.manchester.cs.diff; import java.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.semanticweb.owl.explanation.api.Explanation; import org.semanticweb.owlapi.model.AxiomType; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLAxiom; import org.semanticweb.owlapi.model.OWLDisjointClassesAxiom; import org.semanticweb.owlapi.model.OWLEntity; import org.semanticweb.owlapi.model.OWLImportsDeclaration; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.RemoveAxiom; import org.semanticweb.owlapi.model.RemoveImport; import org.semanticweb.owlapi.util.OWLEntityURIConverter; import org.semanticweb.owlapi.util.OWLEntityURIConverterStrategy; import uk.ac.manchester.cs.diff.EccoSettings.Transformer; import uk.ac.manchester.cs.diff.axiom.CategoricalDiff; import uk.ac.manchester.cs.diff.axiom.LogicalDiffConcurrent; import uk.ac.manchester.cs.diff.axiom.StructuralDiff; import uk.ac.manchester.cs.diff.axiom.changeset.AxiomChangeSet; import uk.ac.manchester.cs.diff.axiom.changeset.CategorisedChangeSet; import uk.ac.manchester.cs.diff.axiom.changeset.LogicalChangeSet; import uk.ac.manchester.cs.diff.axiom.changeset.StructuralChangeSet; import uk.ac.manchester.cs.diff.concept.ContentCVSDiff; import uk.ac.manchester.cs.diff.concept.GrammarDiff; import uk.ac.manchester.cs.diff.concept.SubconceptDiff; import uk.ac.manchester.cs.diff.concept.changeset.ConceptChangeSet; import uk.ac.manchester.cs.diff.output.OutputHandler; import uk.ac.manchester.cs.diff.output.csv.CSVDiffReport; import uk.ac.manchester.cs.diff.output.xml.XMLAxiomDiffReport; import uk.ac.manchester.cs.diff.output.xml.XMLConceptDiffReport; import uk.ac.manchester.cs.diff.output.xml.XMLDiffReport; import uk.ac.manchester.cs.diff.output.xml.XMLUnifiedDiffReport; import uk.ac.manchester.cs.diff.unity.WitnessJustifier; import uk.ac.manchester.cs.diff.unity.changeset.AlignedChangeSet; import uk.ac.manchester.cs.diff.unity.changeset.AlignedDirectChangeSet; import uk.ac.manchester.cs.diff.unity.changeset.AlignedIndirectChangeSet; /** * @author Rafael S. Goncalves <br> * Stanford Center for Biomedical Informatics Research (BMIR) <br> * School of Medicine, Stanford University <br> */ public class Ecco { public static final String outputDir = "ecco-output" + File.separator; private boolean inputChecked; private OWLOntology ont1, ont2; private EccoSettings settings; private AxiomChangeSet axiomChangeSet; private ConceptChangeSet conceptChangeSet; private AlignedChangeSet alignedChangeSet; /** * Constructor * @param ont1 Ontology 1 * @param ont2 Ontology 2 */ public Ecco(OWLOntology ont1, OWLOntology ont2) { this(ont1, ont2, new EccoSettings()); } /** * Constructor * @param ont1 Ontology 1 * @param ont2 Ontology 2 * @param settings ecco settings */ public Ecco(OWLOntology ont1, OWLOntology ont2, EccoSettings settings) { this.ont1 = ont1; this.ont2 = ont2; this.settings = settings; } /** * Compute diff */ public void computeDiff() { verifyInput(); OutputHandler outputHandler = new OutputHandler(settings); XMLDiffReport diffReport = compute(); if(settings.isSavingDocuments()) { outputHandler.saveXMLDocuments(diffReport, false); outputHandler.copySupportingDocuments(); outputHandler.saveStringToFile(settings.getOutputDirectory(), "eccoLog.csv", getCSVLog(diffReport)); } } /** * Compute diff as specified in ecco settings between given ontologies * @return XML diff report */ private XMLDiffReport compute() { long start = System.currentTimeMillis(); XMLDiffReport diffReport = null; axiomChangeSet = getAxiomChanges(); conceptChangeSet = getConceptChanges(); if(axiomChangeSet != null && conceptChangeSet != null) { if(axiomChangeSet instanceof CategorisedChangeSet) { alignedChangeSet = getAlignedChanges(conceptChangeSet, (CategorisedChangeSet)axiomChangeSet); diffReport = getDiffXMLReport(axiomChangeSet, alignedChangeSet); } } if(axiomChangeSet != null) { settings.setTransformer(Transformer.AXIOM); diffReport = getDiffXMLReport(axiomChangeSet); } else if(conceptChangeSet != null) { settings.setTransformer(Transformer.CONCEPT); diffReport = getDiffXMLReport(conceptChangeSet); } long end = System.currentTimeMillis(); System.out.println("-\nfinished diff (total time: " + (end-start)/1000.0 + " seconds)\n"); return diffReport; } /** * Check whether the given ontologies are structurally equivalent w.r.t. OWL 2's notion of structural equivalence * @return true if ontologies are structurally equivalent, false otherwise */ public boolean areStructurallyEquivalent() { if(!inputChecked) verifyInput(); if(axiomChangeSet == null) axiomChangeSet = getStructuralAxiomChanges(); return axiomChangeSet.isEmpty(); } /** * Check whether the given ontologies are logically equivalent * @return true if ontologies are logically equivalent, false otherwise */ public boolean areLogicallyEquivalent() { if(!inputChecked) verifyInput(); if(axiomChangeSet == null) axiomChangeSet = getLogicalAxiomChanges(); else if(axiomChangeSet instanceof StructuralChangeSet) axiomChangeSet = getLogicalAxiomChanges(); else if(axiomChangeSet instanceof CategorisedChangeSet) return ((CategorisedChangeSet)axiomChangeSet).isFreeOfEffectualChanges(); return axiomChangeSet.isEmpty(); } /** * Check whether any changes to the meaning of concepts were found * @return true if the meaning of one or more concepts was changed, false otherwise */ public boolean foundChangesToConcepts() { if(!inputChecked) verifyInput(); if(conceptChangeSet == null) conceptChangeSet = getConceptChanges(); return conceptChangeSet.isEmpty(); } /** * Get XML diff report of an aligned change set * @param axiomChangeSet Axiom change set * @param alignedChangeSet Aligned change set * @return XML diff report */ public XMLDiffReport getDiffXMLReport(AxiomChangeSet axiomChangeSet, AlignedChangeSet alignedChangeSet) { return new XMLUnifiedDiffReport(ont1, ont2, axiomChangeSet, alignedChangeSet); } /** * Get XML diff report of an axiom change set * @param axiomChangeSet Axiom change set * @return XML diff report */ public XMLDiffReport getDiffXMLReport(AxiomChangeSet axiomChangeSet) { return new XMLAxiomDiffReport(ont1, ont2, axiomChangeSet); } /** * Get XML diff report of a concept change set * @param conceptChangeSet Concept change set * @return XML diff report */ public XMLDiffReport getDiffXMLReport(ConceptChangeSet conceptChangeSet) { return new XMLConceptDiffReport(conceptChangeSet); } /** * Compute aligned change set * @param conceptChanges Concept change set * @param axiomChanges Axiom change set * @return Aligned change set */ public AlignedChangeSet getAlignedChanges(ConceptChangeSet conceptChanges, CategorisedChangeSet axiomChanges) { if(alignedChangeSet != null) return alignedChangeSet; if(!inputChecked) verifyInput(); long t2 = System.currentTimeMillis(); System.out.println("Aligning concept and axiom changes... "); System.out.print(" Computing justifications for (lost entailment) witness axioms... "); Map<OWLAxiom,Set<Explanation<OWLAxiom>>> ont1witJusts = new WitnessJustifier(ont1, conceptChanges, settings.getNumberOfJustifications(), "lhs").getJustifications(); long t3 = System.currentTimeMillis(); System.out.println("done (" + (t3-t2)/1000.0 + " secs)"); System.out.print(" Computing justifications for (new entailment) witness axioms... "); Map<OWLAxiom,Set<Explanation<OWLAxiom>>> ont2witJusts = new WitnessJustifier(ont2, conceptChanges, settings.getNumberOfJustifications(), "rhs").getJustifications(); System.out.println("done (" + (System.currentTimeMillis()-t3)/1000.0 + " secs)"); AlignedDirectChangeSet dirChanges = new AlignedDirectChangeSet(axiomChanges, conceptChanges, ont1witJusts, ont2witJusts); AlignedIndirectChangeSet indirChanges = new AlignedIndirectChangeSet(axiomChanges, conceptChanges, ont1witJusts, ont2witJusts); return new AlignedChangeSet(dirChanges, indirChanges); } /** * Compute concept-based diff * @return Concept change set */ public ConceptChangeSet getConceptChanges() { if(!inputChecked) verifyInput(); switch(settings.getConceptDiffType()) { case ATOMIC: return getAtomicChanges(); case SUBCONCEPT: return getSubconceptChanges(); case GRAMMAR: return getGrammarBasedChanges(); case CONTENTCVS: return getContentCvsBasedChanges(); default: return null; } } /** * Compute concept-based diff that checks which concepts have different atomic sub- or super-concepts * @return Concept change set */ private ConceptChangeSet getAtomicChanges() { if(!inputChecked) verifyInput(); SubconceptDiff atomic_diff = new SubconceptDiff(ont1, ont2, settings.isVerbose()); atomic_diff.setAtomicConceptDiff(true); return atomic_diff.getDiff(); } /** * Compute concept-based diff that checks which concepts have different (possibly complex) sub- or super-concepts. * The set of concepts taken into account is composed of asserted complex concepts mentioned in axioms of either ontology * @return Concept change set */ private ConceptChangeSet getSubconceptChanges() { if(!inputChecked) verifyInput(); SubconceptDiff subconcept_diff = new SubconceptDiff(ont1, ont2, settings.isVerbose()); return subconcept_diff.getDiff(); } /** * Compute concept-based diff that checks which concepts have different complex sub- or super-concepts according to the most * expressive entailment grammar. The set of complex concepts taken into account is composed of complex concepts of the form: * C and D, not C, C or D, r some C, r only C. Where C, D are (possibly complex) concepts mentioned in axioms of either ontology, * and r is an atomic role. This task can be very computationally demanding for large input * @return Concept change set */ private ConceptChangeSet getGrammarBasedChanges() { if(!inputChecked) verifyInput(); GrammarDiff grammar_diff = new GrammarDiff(ont1, ont2, settings.isVerbose()); return grammar_diff.getDiff(); } /** * Compute concept-based diff according to ContentCVS's entailment grammar, which is similar to <computeGrammarDiff> but replaces * concepts with atomic concepts only, i.e., it does not take into account asserted complex concepts * @return Concept change set */ private ConceptChangeSet getContentCvsBasedChanges() { if(!inputChecked) verifyInput(); ContentCVSDiff contentcvs_diff = new ContentCVSDiff(ont1, ont2, settings.isVerbose()); return contentcvs_diff.getDiff(); } /** * Compute axiom based diff specified in the settings * @return Axiom change set */ public AxiomChangeSet getAxiomChanges() { if(!inputChecked) verifyInput(); switch(settings.getAxiomDiffType()) { case STRUCTURAL: return getStructuralAxiomChanges(); case LOGICAL: return getLogicalAxiomChanges(); case CATEGORICAL: return getCategorisedAxiomChanges(); default: return null; } } /** * Compute axiom-based structural diff between ontologies, i.e., whether these are structurally * equivalent according to OWL 2's notion of structural equivalence * @return Structural change set */ public StructuralChangeSet getStructuralAxiomChanges() { if(axiomChangeSet != null && axiomChangeSet instanceof StructuralChangeSet) return (StructuralChangeSet)axiomChangeSet; if(!inputChecked) verifyInput(); StructuralDiff structural_diff = new StructuralDiff(ont1, ont2, settings.isVerbose()); return structural_diff.getDiff(); } /** * Compute axiom-based logical diff between ontologies, i.e. verifies whether the given ontologies * are logically equivalent * @return Logical change set */ public LogicalChangeSet getLogicalAxiomChanges() { if(axiomChangeSet != null && axiomChangeSet instanceof LogicalChangeSet) return (LogicalChangeSet)axiomChangeSet; if(!inputChecked) verifyInput(); LogicalDiffConcurrent logical_diff = new LogicalDiffConcurrent(ont1, ont2, settings.isVerbose()); return logical_diff.getDiff(); } /** * Compute axiom-based diff that categorises axiom changes between ontologies according to their impact * @return Categorised change set */ public CategorisedChangeSet getCategorisedAxiomChanges() { if(axiomChangeSet != null && axiomChangeSet instanceof CategorisedChangeSet) return (CategorisedChangeSet)axiomChangeSet; if(!inputChecked) verifyInput(); CategoricalDiff categorical_diff = new CategoricalDiff(ont1, ont2, settings.getNumberOfJustifications(), settings.isVerbose()); return categorical_diff.getDiff(); } /** * Get CSV log of changes found * @param diffReport XML diff report * @return CSV log of change types and number */ public String getCSVLog(XMLDiffReport diffReport) { CSVDiffReport csvReport = new CSVDiffReport(diffReport); return csvReport.getCSV(); } /** * Check whether input to ecco needs alterations. By default, ecco removes * unary disjointness axioms, since dependent tools do not like these */ private void verifyInput() { removeUnaryDisjointnessAxioms(ont1); removeUnaryDisjointnessAxioms(ont2); if(settings.isIgnoringAbox()) { removeAbox(ont1); removeAbox(ont2); } if(!settings.isProcessingImports()) { removeImports(ont1); removeImports(ont2); } else { inflateOntologyWithImports(ont1); inflateOntologyWithImports(ont2); } if(settings.isNormalizingURIs()) { normalizeEntityURIs(ont1); normalizeEntityURIs(ont2); } inputChecked = true; } /** * Normalize entity URIs, e.g.: if there exists an entity named "A" in sig(O1) and sig(O2) but in different namespaces, * the diff would normally report changes involving "A", which is not always desirable. This method will rename all entities * in the given ontology to a common namespace * @param ont OWL ontology */ private void normalizeEntityURIs(OWLOntology ont) { if(settings.isVerbose()) System.out.print(" Normalizing entity URIs... "); Set<OWLOntology> ontSet = new HashSet<OWLOntology>(); ontSet.add(ont); OWLEntityURIConverter converter = new OWLEntityURIConverter(ont.getOWLOntologyManager(), ontSet, new OWLEntityURIConverterStrategy() { @Override public IRI getConvertedIRI(OWLEntity e) { String entityName = getShortForm(e.getIRI()); IRI iri = IRI.create("http://owl.cs.manchester.ac.uk/ecco#" + entityName); return iri; } }); ont.getOWLOntologyManager().applyChanges(converter.getChanges()); if(settings.isVerbose()) System.out.println("done"); } /** * Get the short form an IRI * @param iri IRI * @return Short form of the IRI, i.e., only entity name */ private String getShortForm(IRI iri) { return iri.toString().substring(iri.toString().lastIndexOf("#")+1); } /** * Remove unary disjointness axioms from the given ontology * @param ont OWL ontology */ private void removeUnaryDisjointnessAxioms(OWLOntology ont) { List<RemoveAxiom> toRemove = new ArrayList<RemoveAxiom>(); for(OWLAxiom ax : ont.getAxioms(AxiomType.DISJOINT_CLASSES)) { OWLDisjointClassesAxiom dis = (OWLDisjointClassesAxiom)ax; if(dis.getClassesInSignature().size() < 2) toRemove.add(new RemoveAxiom(ont, ax)); } ont.getOWLOntologyManager().applyChanges(toRemove); } /** * Remove Abox axioms from given ontology * @param ont Ontology to remove Abox axioms from */ private void removeAbox(OWLOntology ont) { Set<OWLAxiom> aboxAxs = ont.getABoxAxioms(true); List<RemoveAxiom> toRemove = new ArrayList<RemoveAxiom>(); for(OWLAxiom ax : aboxAxs) toRemove.add(new RemoveAxiom(ont, ax)); ont.getOWLOntologyManager().applyChanges(toRemove); } /** * Remove all imports from the given ontology * @param ont OWL ontology */ private void removeImports(OWLOntology ont) { for(OWLImportsDeclaration importDecl : ont.getImportsDeclarations()) ont.getOWLOntologyManager().applyChange(new RemoveImport(ont.getOWLOntologyManager().getImportedOntology(importDecl), importDecl)); } /** * Add all axioms of imported ontologies to the parent ontology, and remove the imports pointers * @param ont OWL ontology */ private void inflateOntologyWithImports(OWLOntology ont) { for(OWLOntology imported : ont.getImports()) ont.getOWLOntologyManager().applyChanges(ont.getOWLOntologyManager().addAxioms(ont, imported.getAxioms())); removeImports(ont); } }