diff --git a/docs/dl-query.md b/docs/dl-query.md new file mode 100644 index 000000000..23f93c121 --- /dev/null +++ b/docs/dl-query.md @@ -0,0 +1,29 @@ +# DL-Query + +## Contents + +1. [Overview](#overview) +2. [Query types](#query-types) + +## Overview + +ROBOT can execute DL queries against an ontology. The functionality closely mimics the functionality of the DL Query Tab in Protege. + +The `dl-query` command can be used to query for ancestors, descendants, instances and other relatives of an OWL Class Expression that is provided in Manchester syntax. + +The output is always a list of Entity IRIs. Multiple queries and output files can be supplied. For example: + + robot query --input uberon_module.owl \ + --query "'part_of' some 'subdivision of trunk'" part_of_subdiv_trunk.txt \ + --query "'part_of' some 'nervous system'" part_of_nervous_system.txt + +## Query Types + +The following query types are currently supported: + +- equivalents: Classes that are exactly equivalent to the supplied class expression +- parents: Direct parents (superclasses) of the class expression provided +- children: Direct children (subclasses) of the class expression provided +- descendants (default): All subclasses of the class expression provided +- ancestors: All superclasses of the class expression provided +- instances: All named individuals that are instances of the class expression provided diff --git a/robot-command/src/main/java/org/obolibrary/robot/DLQueryCommand.java b/robot-command/src/main/java/org/obolibrary/robot/DLQueryCommand.java new file mode 100644 index 000000000..9f3d561b9 --- /dev/null +++ b/robot-command/src/main/java/org/obolibrary/robot/DLQueryCommand.java @@ -0,0 +1,174 @@ +package org.obolibrary.robot; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.io.FileUtils; +import org.obolibrary.robot.exceptions.InconsistentOntologyException; +import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.model.*; +import org.semanticweb.owlapi.reasoner.OWLReasoner; +import org.semanticweb.owlapi.reasoner.OWLReasonerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles inputs and outputs for the {@link DLQueryOperation}. + * + * @author Nicolas Matentzoglu + */ +public class DLQueryCommand implements Command { + /** Logger. */ + private static final Logger logger = LoggerFactory.getLogger(DLQueryCommand.class); + + private static final String NS = "dl-query#"; + + private static final List LEGAL_RELATIONS = + Arrays.asList("equivalents", "ancestors", "descendants", "instances", "parents", "children"); + OWLDataFactory df = OWLManager.getOWLDataFactory(); + + private static final String maxTypeError = NS + "MAX TYPE ERROR --max ('%s') must be an integer"; + private static final String illegalRelationError = + NS + "ILLEGAL RELATION ERROR: %s. Must be one of " + String.join(" ", LEGAL_RELATIONS) + "."; + private static final String missingQueryArgumentError = + NS + "MISSING QUERY ARGUMENT ERROR: must have a valid --query."; + + /** Error message when --query does not have two arguments. */ + private static final String missingOutputError = + NS + "MISSING OUTPUT ERROR --%s requires two arguments: query and output"; + + /** Error message when a query is not provided */ + private static final String missingQueryError = + NS + "MISSING QUERY ERROR at least one query must be provided"; + + /** Store the command-line options for the command. */ + private Options options; + + public DLQueryCommand() { + Options o = CommandLineHelper.getCommonOptions(); + o.addOption("i", "input", true, "load ontology from a file"); + o.addOption("I", "input-iri", true, "load ontology from an IRI"); + o.addOption("r", "reasoner", true, "reasoner to use: ELK, HermiT, JFact"); + + Option opt = new Option("q", "query", true, "the DL query to run"); + opt.setArgs(2); + o.addOption(opt); + + o.addOption( + "s", + "select", + true, + "select what relations to query: equivalents, parents, children, ancestors, descendants, instances"); + o.addOption("o", "output", true, "save ontology containing only explanation axioms to a file"); + options = o; + } + + @Override + public String getName() { + return "dl-query"; + } + + @Override + public String getDescription() { + return "query the ontology with the given class expression"; + } + + @Override + public String getUsage() { + return "robot dl-query --input --query --output "; + } + + @Override + public Options getOptions() { + return options; + } + + /** + * Handle the command-line and file operations for the DLQueryOperation. + * + * @param args strings to use as arguments + */ + @Override + public void main(String[] args) { + try { + execute(null, args); + } catch (Exception e) { + CommandLineHelper.handleException(e); + } + } + + @Override + public CommandState execute(CommandState state, String[] args) throws Exception { + CommandLine line = CommandLineHelper.getCommandLine(getUsage(), getOptions(), args); + if (line == null) { + return null; + } + if (state == null) { + state = new CommandState(); + } + IOHelper ioHelper = CommandLineHelper.getIOHelper(line); + state = CommandLineHelper.updateInputOntology(ioHelper, state, line); + OWLOntology ontology = state.getOntology(); + + OWLReasonerFactory reasonerFactory = CommandLineHelper.getReasonerFactory(line, true); + List selects = CommandLineHelper.getOptionalValues(line, "select"); + + List> queries = getQueries(line); + for (List q : queries) { + queryOntology(q, ontology, reasonerFactory, selects); + } + + state.setOntology(ontology); + CommandLineHelper.maybeSaveOutput(line, ontology); + return state; + } + + private void queryOntology( + List q, + OWLOntology ontology, + OWLReasonerFactory reasonerFactory, + List selects) + throws InconsistentOntologyException, IOException { + OWLReasoner r = reasonerFactory.createReasoner(ontology); + String query = q.get(0); + File output = new File(q.get(1)); + OWLClassExpression classExpression = DLQueryOperation.parseOWLClassExpression(query, ontology); + if (r.isConsistent()) { + List entities = DLQueryOperation.query(classExpression, r, selects); + writeQueryResultsToFile(output, entities); + } else { + throw new InconsistentOntologyException(); + } + } + + /** + * Given a command line, get a list of queries. + * + * @param line CommandLine with options + * @return List of queries + */ + private static List> getQueries(CommandLine line) { + // Collect all queries as (queryPath, outputPath) pairs. + List> queries = new ArrayList<>(); + List qs = CommandLineHelper.getOptionalValues(line, "query"); + for (int i = 0; i < qs.size(); i += 2) { + try { + queries.add(qs.subList(i, i + 2)); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException(String.format(missingOutputError, "query")); + } + } + if (queries.isEmpty()) { + throw new IllegalArgumentException(missingQueryError); + } + return queries; + } + + private void writeQueryResultsToFile(File output, List results) throws IOException { + Collections.sort(results); + FileUtils.writeLines(output, results); + } +} diff --git a/robot-command/src/main/java/org/obolibrary/robot/QueryCommand.java b/robot-command/src/main/java/org/obolibrary/robot/QueryCommand.java index a1a74442c..eb2ab99e2 100644 --- a/robot-command/src/main/java/org/obolibrary/robot/QueryCommand.java +++ b/robot-command/src/main/java/org/obolibrary/robot/QueryCommand.java @@ -15,7 +15,10 @@ import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.tdb.TDBFactory; +import org.phenoscape.owlet.Owlet; import org.semanticweb.owlapi.model.OWLOntology; +import org.semanticweb.owlapi.reasoner.OWLReasoner; +import org.semanticweb.owlapi.reasoner.OWLReasonerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -239,14 +242,27 @@ private static void executeInMemory( CommandLine line, OWLOntology inputOntology, List> queries) throws Exception { boolean useGraphs = CommandLineHelper.getBooleanValue(line, "use-graphs", false); Dataset dataset = QueryOperation.loadOntologyAsDataset(inputOntology, useGraphs); + boolean useOwlet = CommandLineHelper.getBooleanValue(line, "owlet", false); + + Owlet owlet = getOwlet(line, inputOntology, useOwlet); try { - runQueries(line, dataset, queries); + runQueries(line, dataset, queries, owlet); } finally { dataset.close(); // TDBFactory.release(dataset); } } + private static Owlet getOwlet(CommandLine line, OWLOntology inputOntology, boolean useOwlet) { + Owlet owlet = null; + if (useOwlet) { + OWLReasonerFactory rf = CommandLineHelper.getReasonerFactory(line); + OWLReasoner r = rf.createReasoner(inputOntology); + owlet = new Owlet(r); + } + return owlet; + } + /** * Given a command line and a list of queries, execute 'query' using TDB and writing mappings to * disk. @@ -260,8 +276,9 @@ private static void executeOnDisk(CommandLine line, List> queries) Dataset dataset = createTDBDataset(line); boolean keepMappings = CommandLineHelper.getBooleanValue(line, "keep-tdb-mappings", false); String tdbDir = CommandLineHelper.getDefaultValue(line, "tdb-directory", ".tdb"); + try { - runQueries(line, dataset, queries); + runQueries(line, dataset, queries, null); } finally { dataset.close(); TDBFactory.release(dataset); @@ -396,7 +413,8 @@ private static List> getQueries(CommandLine line) { * @param queries List of queries * @throws IOException on issue reading or writing files */ - private static void runQueries(CommandLine line, Dataset dataset, List> queries) + private static void runQueries( + CommandLine line, Dataset dataset, List> queries, Owlet owlet) throws IOException { String format = CommandLineHelper.getOptionalValue(line, "format"); String outputDir = CommandLineHelper.getDefaultValue(line, "output-dir", ""); @@ -407,6 +425,12 @@ private static void runQueries(CommandLine line, Dataset dataset, List org.apache.jena jena-arq - 3.17.0 + 4.9.0 com.github.jsonld-java @@ -134,7 +134,7 @@ org.apache.jena jena-tdb - 3.17.0 + 4.9.0 org.apache.poi @@ -232,6 +232,11 @@ jackson-annotations 2.12.2 + + com.fasterxml.jackson.core + jackson-databind + 2.12.2 + net.sourceforge.owlapi owlexplanation @@ -288,6 +293,17 @@ + + org.phenoscape + owlet_${scala.version} + 2.0.0 + + + net.sourceforge.owlapi + owlapi-distribution + + + com.google.code.gson gson diff --git a/robot-core/src/main/java/org/obolibrary/robot/DLQueryOperation.java b/robot-core/src/main/java/org/obolibrary/robot/DLQueryOperation.java new file mode 100644 index 000000000..35032d565 --- /dev/null +++ b/robot-core/src/main/java/org/obolibrary/robot/DLQueryOperation.java @@ -0,0 +1,92 @@ +package org.obolibrary.robot; + +import java.util.*; +import org.semanticweb.owlapi.apibinding.OWLManager; +import org.semanticweb.owlapi.expression.ShortFormEntityChecker; +import org.semanticweb.owlapi.manchestersyntax.parser.ManchesterOWLSyntaxClassExpressionParser; +import org.semanticweb.owlapi.manchestersyntax.renderer.ParserException; +import org.semanticweb.owlapi.model.*; +import org.semanticweb.owlapi.reasoner.OWLReasoner; +import org.semanticweb.owlapi.util.BidirectionalShortFormProviderAdapter; +import org.semanticweb.owlapi.util.SimpleShortFormProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Query the ontology with a class expression. This simulates the DL Query Tab in Protege. + * + * @author Nicolas Matentzoglu + */ +public class DLQueryOperation { + + /** Logger */ + private static final Logger logger = LoggerFactory.getLogger(DLQueryOperation.class); + + private static final OWLDataFactory df = OWLManager.getOWLDataFactory(); + + /** + * Query the ontology with the provided class expression. + * + * @param expression the expression to be queried + * @param reasoner the initialised reasoner to query + * @param queryTypes the query results to be included (parents, children, equivants) + * @return explanations + */ + public static List query( + OWLClassExpression expression, OWLReasoner reasoner, List queryTypes) { + logger.debug("Querying: " + expression); + List results = new ArrayList<>(); + if (queryTypes.isEmpty()) { + logger.info("No query type supplied, using 'descendants'"); + queryTypes.add("descendants"); + } + + for (String type : queryTypes) { + switch (type) { + case "equivalents": + reasoner.getEquivalentClasses(expression).getEntities().forEach(results::add); + break; + case "parents": + reasoner.getSuperClasses(expression, true).getFlattened().forEach(results::add); + results.remove(df.getOWLThing()); + break; + case "children": + reasoner.getSubClasses(expression, true).getFlattened().forEach(results::add); + results.remove(df.getOWLNothing()); + break; + case "ancestors": + reasoner.getSuperClasses(expression, false).getFlattened().forEach(results::add); + results.remove(df.getOWLThing()); + break; + case "descendants": + reasoner.getSubClasses(expression, false).getFlattened().forEach(results::add); + results.remove(df.getOWLNothing()); + break; + case "instances": + reasoner.getInstances(expression, false).getFlattened().forEach(results::add); + break; + default: + throw new IllegalArgumentException(type + " is not a legal query relation for dl-query"); + } + } + return results; + } + + public static OWLClassExpression parseOWLClassExpression(String expression, OWLOntology ontology) + throws ParserException { + OWLClassExpression classExpression = null; + BidirectionalShortFormProviderAdapter shortFormProvider = + new BidirectionalShortFormProviderAdapter( + OWLManager.createOWLOntologyManager(), + Collections.singleton(ontology), + new SimpleShortFormProvider()); + + ManchesterOWLSyntaxClassExpressionParser parser = + new ManchesterOWLSyntaxClassExpressionParser( + ontology.getOWLOntologyManager().getOWLDataFactory(), + new ShortFormEntityChecker(shortFormProvider)); + + classExpression = parser.parse(expression); + return classExpression; + } +} diff --git a/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java b/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java index 9362530cc..9446d3420 100644 --- a/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java +++ b/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java @@ -486,7 +486,7 @@ public static Lang getFormatLang(String formatName) { formatName = formatName.toLowerCase(); switch (formatName) { case "tsv": - format = ResultSetLang.SPARQLResultSetTSV; + format = ResultSetLang.RS_TSV; break; case "ttl": format = Lang.TTL; @@ -507,7 +507,7 @@ public static Lang getFormatLang(String formatName) { format = Lang.RDFXML; break; case "sxml": - format = ResultSetLang.SPARQLResultSetXML; + format = ResultSetLang.RS_XML; break; default: format = null;