/** * Copyright (c) 2015 Lemur Consulting Ltd. * <p/> * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package uk.co.flax.biosolr.elasticsearch.mapper.ontology.owl; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.semanticweb.owlapi.apibinding.OWLManager; import org.semanticweb.owlapi.model.IRI; import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom; import org.semanticweb.owlapi.model.OWLAnnotationProperty; import org.semanticweb.owlapi.model.OWLAnnotationValue; import org.semanticweb.owlapi.model.OWLClass; import org.semanticweb.owlapi.model.OWLClassExpression; import org.semanticweb.owlapi.model.OWLDataFactory; import org.semanticweb.owlapi.model.OWLLiteral; import org.semanticweb.owlapi.model.OWLObjectProperty; import org.semanticweb.owlapi.model.OWLObjectSomeValuesFrom; import org.semanticweb.owlapi.model.OWLOntology; import org.semanticweb.owlapi.model.OWLOntologyCreationException; import org.semanticweb.owlapi.model.OWLOntologyManager; import org.semanticweb.owlapi.model.OWLSubClassOfAxiom; import org.semanticweb.owlapi.reasoner.Node; import org.semanticweb.owlapi.reasoner.NodeSet; import org.semanticweb.owlapi.reasoner.OWLReasoner; import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.co.flax.biosolr.elasticsearch.mapper.ontology.OntologySettings; /** * Helper class for loading an ontology and making its properties easily * accessible. * * @author mlp */ public class OntologyHelper { private static final Logger LOGGER = LoggerFactory.getLogger(OntologyHelper.class); private final OntologySettings config; private final URI ontologyUri; private final OWLOntology ontology; private final OWLReasoner reasoner; // private final ShortFormProvider shortFormProvider; private final IRI owlNothingIRI; private final Map<IRI, OWLClass> owlClassMap = new HashMap<>(); private Map<IRI, Collection<String>> labels = new HashMap<>(); private Map<IRI, Collection<String>> synonyms = new HashMap<>(); private Map<IRI, Collection<String>> definitions = new HashMap<>(); private long lastCallTime; /** * Construct a new ontology helper instance with a string representing the * ontology URI. * * @param ontologyUriString the URI. * @param config the ontology configuration, containing the property URIs * for labels, synonyms, etc. * @throws OWLOntologyCreationException if the ontology cannot be read for * some reason - internal inconsistencies, etc. * @throws URISyntaxException if the URI cannot be parsed. */ public OntologyHelper(OntologySettings config) throws OWLOntologyCreationException, URISyntaxException { this(new URI(config.getOntologyUri()), config); } /** * Construct a new ontology helper instance. * * @param ontologyUri the URI giving the location of the ontology. * @param config the ontology configuration, containing the property URIs * for labels, synonyms, etc. * @throws OWLOntologyCreationException if the ontology cannot be read for * some reason - internal inconsistencies, etc. * @throws URISyntaxException if the URI cannot be parsed. */ public OntologyHelper(URI ontologyUri, OntologySettings config) throws OWLOntologyCreationException, URISyntaxException { this.config = config; if (!ontologyUri.isAbsolute()) { // Try to read as a file from the resource path LOGGER.debug("Ontology URI {} is not absolute - loading from classpath", ontologyUri); URL url = this.getClass().getClassLoader().getResource(ontologyUri.toString()); if (url != null) { ontologyUri = url.toURI(); } } this.ontologyUri = ontologyUri; LOGGER.info("Loading ontology from " + ontologyUri + "..."); OWLOntologyManager manager = OWLManager.createOWLOntologyManager(); IRI iri = IRI.create(ontologyUri); this.ontology = manager.loadOntologyFromOntologyDocument(iri); // Use a buffering reasoner - not interested in ongoing changes this.reasoner = new StructuralReasonerFactory().createReasoner(ontology); // this.shortFormProvider = new SimpleShortFormProvider(); this.owlNothingIRI = manager.getOWLDataFactory().getOWLNothing().getIRI(); // Initialise the class map initialiseClassMap(); } private void initialiseClassMap() { for (OWLClass clazz : ontology.getClassesInSignature()) { owlClassMap.put(clazz.getIRI(), clazz); } } /** * Explicitly dispose of the helper class, closing down any resources in * use. */ public void dispose() { if (reasoner != null) { LOGGER.info("Disposing of ontology reasoner for {}", ontologyUri); reasoner.dispose(); } } /** * Get the OWL class for an IRI. * * @param iri the IRI of the required class. * @return the class from the ontology, or <code>null</code> if no such * class can be found, or the IRI string is null. */ public OWLClass getOwlClass(String iri) { OWLClass ret = null; if (StringUtils.isNotBlank(iri)) { ret = owlClassMap.get(IRI.create(iri)); } return ret; } /** * Find the labels for a single OWL class. * * @param owlClass the class whose labels are required. * @return a collection of labels for the class. Never <code>null</code>. */ public Collection<String> findLabels(OWLClass owlClass) { return findLabels(owlClass.getIRI()); } /** * Find all of the labels for a collection of OWL class IRIs. * * @param iris the IRIs whose labels should be looked up. * @return a collection of labels. Never <code>null</code>. */ public Collection<String> findLabelsForIRIs(Collection<String> iris) { Set<String> labels = new HashSet<>(); iris.stream().map(iri -> findLabels(IRI.create(iri))).forEach(labels::addAll); return labels; } /** * Find all of the synonyms for an OWL class. * * @param owlClass * @return the synonyms. Never <code>null</code>. */ public Collection<String> findSynonyms(OWLClass owlClass) { return findSynonyms(owlClass.getIRI()); } /** * Find all of the definitions for an OWL class. * * @param owlClass * @return the definitions. Never <code>null</code>. */ public Collection<String> findDefinitions(OWLClass owlClass) { return findDefinitions(owlClass.getIRI()); } private Collection<String> findLabels(IRI iri) { if (!labels.containsKey(iri)) { Collection<String> classNames = findPropertyValueStrings(config.getLabelPropertyUris(), iri); labels.put(iri, classNames); } return labels.get(iri); } private Collection<String> findSynonyms(IRI iri) { if (!synonyms.containsKey(iri)) { Collection<String> classNames = findPropertyValueStrings(config.getSynonymPropertyUris(), iri); synonyms.put(iri, classNames); } return synonyms.get(iri); } private Collection<String> findDefinitions(IRI iri) { if (!definitions.containsKey(iri)) { Collection<String> classNames = findPropertyValueStrings(config.getDefinitionPropertyUris(), iri); definitions.put(iri, classNames); } return definitions.get(iri); } private Collection<String> findPropertyValueStrings(List<String> propertyUris, IRI iri) { Collection<String> classNames = new HashSet<>(); OWLDataFactory odf = ontology.getOWLOntologyManager().getOWLDataFactory(); // For every property URI, find the annotations for this entry propertyUris.parallelStream().map(uri -> odf.getOWLAnnotationProperty(IRI.create(uri))) .map(prop -> findAnnotationNames(iri, prop)).forEach(classNames::addAll); return classNames; } private Collection<String> findAnnotationNames(IRI iri, OWLAnnotationProperty annotationType) { Collection<String> classNames = new HashSet<String>(); // get all literal annotations for (OWLAnnotationAssertionAxiom axiom : ontology.getAnnotationAssertionAxioms(iri)) { if (axiom.getAnnotation().getProperty().equals(annotationType)) { OWLAnnotationValue value = axiom.getAnnotation().getValue(); Optional<String> name = getOWLAnnotationValueAsString(value); if (name.isPresent()) { classNames.add(name.get()); } } } return classNames; } /** * Get the direct child URIs for an OWL class. * * @param owlClass * @return the child URIs, as strings. Never <code>null</code>. */ public Collection<String> getChildUris(OWLClass owlClass) { return getSubclassUris(owlClass, true); } /** * Get all descendant URIs for an OWL class, including direct children. * * @param owlClass * @return the descendant URIs, as strings. Never <code>null</code>. */ public Collection<String> getDescendantUris(OWLClass owlClass) { return getSubclassUris(owlClass, false); } /** * Get direct parent URIs for an OWL class. * * @param owlClass * @return the parent URIs, as strings. Never <code>null</code>. */ public Collection<String> getParentUris(OWLClass owlClass) { return getSuperclassUris(owlClass, true); } /** * Get all ancestor URIs for an OWL class, including direct parents. * * @param owlClass * @return the ancestor URIs, as strings. Never <code>null</code>. */ public Collection<String> getAncestorUris(OWLClass owlClass) { return getSuperclassUris(owlClass, false); } private Collection<String> getSubclassUris(OWLClass owlClass, boolean direct) { return getUrisFromNodeSet(reasoner.getSubClasses(owlClass, direct)); } private Collection<String> getSuperclassUris(OWLClass owlClass, boolean direct) { return getUrisFromNodeSet(reasoner.getSuperClasses(owlClass, direct)); } private Collection<String> getUrisFromNodeSet(NodeSet<OWLClass> nodeSet) { Set<String> uris = new HashSet<>(); for (Node<OWLClass> node : nodeSet) { for (OWLClass expr : node.getEntities()) { if (isClassSatisfiable(expr)) { uris.add(expr.getIRI().toURI().toString()); } } } return uris; } private boolean isClassSatisfiable(OWLClass owlClass) { return !owlClass.isAnonymous() && !owlClass.getIRI().equals(owlNothingIRI); } /** * Retrieve a map of related classes for a particular class. * * @param owlClass * @return a map of relation type to a list of IRIs for nodes with that * relationship. */ public Map<String, List<String>> getRestrictions(OWLClass owlClass) { RestrictionVisitor visitor = new RestrictionVisitor(Collections.singleton(ontology)); for (OWLSubClassOfAxiom ax : ontology.getSubClassAxiomsForSubClass(owlClass)) { OWLClassExpression superCls = ax.getSuperClass(); // Ask our superclass to accept a visit from the RestrictionVisitor // - if it is an existential restriction then our restriction visitor // will answer it - if not our visitor will ignore it superCls.accept(visitor); } Map<String, List<String>> restrictions = new HashMap<>(); for (OWLObjectSomeValuesFrom val : visitor.getSomeValues()) { OWLClassExpression exp = val.getFiller(); // Get the shortname of the property expression String shortForm = null; Set<OWLObjectProperty> signatureProps = val.getProperty().getObjectPropertiesInSignature(); for (OWLObjectProperty sigProp : signatureProps) { Collection<String> labels = findLabels(sigProp.getIRI()); if (labels.size() > 0) { shortForm = new ArrayList<String>(labels).get(0); } } if (shortForm != null && !exp.isAnonymous()) { IRI iri = exp.asOWLClass().getIRI(); if (!restrictions.containsKey(shortForm)) { restrictions.put(shortForm, new ArrayList<String>()); } restrictions.get(shortForm).add(iri.toString()); } } return restrictions; } private Optional<String> getOWLAnnotationValueAsString(OWLAnnotationValue value) { if (value instanceof IRI) { Optional<String> shortForm = getShortForm((IRI) value); if (shortForm.isPresent()) { return Optional.of(shortForm.get()); } } else if (value instanceof OWLLiteral) { return Optional.of(((OWLLiteral) value).getLiteral()); } return Optional.empty(); } private Optional<String> getShortForm(IRI entityIRI) { LOGGER.trace("Attempting to extract fragment name of URI '" + entityIRI + "'"); String termURI = entityIRI.toString(); URI entUri = entityIRI.toURI(); // we want the "final part" of the URI... if (!StringUtils.isEmpty(entUri.getFragment())) { // a uri with a non-null fragment, so use this... LOGGER.trace("Extracting fragment name using URI fragment (" + entUri.getFragment() + ")"); return Optional.of(entUri.getFragment()); } else if (entityIRI.toURI().getPath() != null) { // no fragment, but there is a path so try and extract the final // part... if (entityIRI.toURI().getPath().contains("/")) { LOGGER.trace("Extracting fragment name using final part of the path of the URI"); return Optional.of(entityIRI.toURI().getPath() .substring(entityIRI.toURI().getPath().lastIndexOf('/') + 1)); } else { // no final path part, so just return whole path LOGGER.trace("Extracting fragment name using the path of the URI"); return Optional.of(entityIRI.toURI().getPath()); } } else { // no fragment, path is null, we've run out of rules so don't // shorten LOGGER.trace("No rules to shorten this URI could be found (" + termURI + ")"); return Optional.empty(); } } /** * Update the record of the last time this class was used. This * method should be called every time the helper class is accessed. */ public void updateLastCallTime() { this.lastCallTime = System.currentTimeMillis(); } /** * @return the last time the helper was called. */ public long getLastCallTime() { return lastCallTime; } public Set<String> getRestrictionProperties() { Set<String> properties = new HashSet<>(); Set<OWLObjectProperty> objectProperties = ontology.getObjectPropertiesInSignature(); for (OWLObjectProperty oop : objectProperties) { List<String> labels = new ArrayList<>(findLabels(oop.getIRI())); if (!labels.isEmpty()) { properties.add(labels.get(0)); } else { Optional<String> shortForm = getShortForm(oop.getIRI()); if (shortForm.isPresent()) { properties.add(shortForm.get()); } } } return properties; } }