Repository: ontodev/robot
Pull request #179 (2017-08-10 23:43:06)

Verify command
for https://github.com/ontodev/robot/issues/177


Patch:
diff --git a/examples/README.md b/examples/README.md
index 18023205..e61125e6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -93,6 +93,70 @@ For more details see:
 - [SLME](http://owlapi.sourceforge.net/javadoc/uk/ac/manchester/cs/owlapi/modularity/SyntacticLocalityModuleExtractor.html)
 - [ModuleType](http://owlapi.sourceforge.net/javadoc/uk/ac/manchester/cs/owlapi/modularity/ModuleType.html)
 
+## Querying
+
+Robot can be used to submit [sparql](https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#bind) 
+queries against an ontology. Robot currently has two commands `query` and `verify` that use sparql to
+perform queries on ontology sets.
+
+### `query`
+
+The `query` command comes in two flavors, `select` and `construct`, which are passed in as options:
+`robot query --select` or `robot query --construct`. `select` corresponds to a sparql 
+_[SELECT](https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#select)_ query. This is used to 
+extract data matching a pattern. 
+
+    robot query --input nucleus.owl --select subclass_of_0044464.sparql output
+
+Resulting in:
+    
+    subs,names
+    http://purl.obolibrary.org/obo/GO_0044424,intracellular part
+    http://purl.obolibrary.org/obo/GO_0005622,intracellular
+    
+The `construct` option allows one to build triples based on a pattern using a 
+[CONSTRUCT](https://www.w3.org/TR/2013/REC-sparql11-query-20130321/#construct) query. This will output
+triples in the format specified with the `--format` option, and should probably be set to "ttl" for this
+query mode. For example:
+
+    robot query --format ttl --input nucleus.owl --construct construct_some.sparql output
+    
+Has many results, but a portion could look like:
+
+    <http://purl.obolibrary.org/obo/GO_0044464>
+            part_of_inferred:  <http://purl.obolibrary.org/obo/GO_0005623> .
+    
+    <http://purl.obolibrary.org/obo/GO_0043229>
+            part_of_inferred:  <http://purl.obolibrary.org/obo/GO_0005622> .
+            
+Note that we've created a triple where none existed before. 
+
+### `verify`
+
+The verify command runs a SELECT sparql query against an ontology where the results of the query are
+considered "violations". Verify expects that there will be no results for a valid ontology. Any results
+are reported in a file and then robot exits with status 1. You can pass in as many queries
+files as you want in to the `--queries` option. They'll each be run against the ontology
+provided. Inside the results (`-O`) directory robot will create output files for each sparql query. The 
+directory passed to `-O` must exist.
+
+For example:
+
+```
+robot verify --input asserted-equiv.owl --queries equivalent.sparql -O results/
+```
+    
+Should output as a response: 
+
+    Rule /Users/edouglass/lbl/ontodev/robot/examples/equivalent.sparql: 1 violation(s)
+    first,second,firstLabel,secondLabel
+    http://purl.obolibrary.org/obo/TEST_A,http://purl.obolibrary.org/obo/TEST_B,,
+    
+And the CSV file results/equivalent.csv should have:
+
+    first,second,firstLabel,secondLabel
+    http://purl.obolibrary.org/obo/TEST_A,http://purl.obolibrary.org/obo/TEST_B,,
+
 
 ## Reasoning
 
diff --git a/examples/construct_some.sparql b/examples/construct_some.sparql
new file mode 100644
index 00000000..b3b9fd70
--- /dev/null
+++ b/examples/construct_some.sparql
@@ -0,0 +1,14 @@
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+PREFIX owl: <http://www.w3.org/2002/07/owl#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX part_of: <http://purl.obolibrary.org/obo/BFO_0000050>
+PREFIX part_of_inferred: <http://purl.obolibrary.org/obo/BFO_0000050_SOME>
+
+CONSTRUCT {
+    ?part part_of_inferred: ?whole
+}
+WHERE {
+    ?part rdfs:subClassOf [rdf:type owl:Restriction ;
+            owl:onProperty part_of: ;
+            owl:someValuesFrom ?whole ]
+}
\ No newline at end of file
diff --git a/examples/equivalent.sparql b/examples/equivalent.sparql
new file mode 100644
index 00000000..fd86a858
--- /dev/null
+++ b/examples/equivalent.sparql
@@ -0,0 +1,13 @@
+prefix owl: <http://www.w3.org/2002/07/owl#>
+prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+
+SELECT ?first ?second ?firstLabel ?secondLabel
+WHERE {
+    ?first owl:equivalentClass ?second .
+
+    OPTIONAL {?first rdfs:label ?firstLabel . }
+    OPTIONAL { ?second rdfs:label ?secondLabel . }
+
+    FILTER (!isBlank(?first))
+    FILTER (!isBlank(?second))
+}
\ No newline at end of file
diff --git a/examples/subclass_of_0044464.sparql b/examples/subclass_of_0044464.sparql
new file mode 100644
index 00000000..a2db9d6c
--- /dev/null
+++ b/examples/subclass_of_0044464.sparql
@@ -0,0 +1,10 @@
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
+PREFIX owl: <http://www.w3.org/2002/07/owl#>
+PREFIX cellPart: <http://purl.obolibrary.org/obo/GO_0044464>
+
+SELECT ?subs ?names
+WHERE {
+    ?subs rdfs:subClassOf cellPart: ;
+        rdfs:label ?names .
+}
\ No newline at end of file
diff --git a/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java b/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java
index 16eff4b6..a171cf44 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/CommandLineInterface.java
@@ -37,6 +37,7 @@ public class CommandLineInterface {
         m.addCommand("template",         new TemplateCommand());
         m.addCommand("unmerge",          new UnmergeCommand());
         m.addCommand("validate-profile", new ValidateProfileCommand());
+        m.addCommand("verify",           new VerifyCommand());
         return m;
     }
 
diff --git a/robot-command/src/main/java/org/obolibrary/robot/VerifyCommand.java b/robot-command/src/main/java/org/obolibrary/robot/VerifyCommand.java
new file mode 100644
index 00000000..30c8fb6b
--- /dev/null
+++ b/robot-command/src/main/java/org/obolibrary/robot/VerifyCommand.java
@@ -0,0 +1,111 @@
+package org.obolibrary.robot;
+
+import com.google.common.io.Files;
+import com.hp.hpl.jena.query.ResultSet;
+import com.hp.hpl.jena.query.ResultSetFactory;
+import com.hp.hpl.jena.query.ResultSetRewindable;
+import com.hp.hpl.jena.sparql.core.DatasetGraph;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.jena.riot.Lang;
+import org.apache.jena.riot.ResultSetMgr;
+import org.obolibrary.robot.exceptions.CannotReadQuery;
+import org.semanticweb.owlapi.apibinding.OWLManager;
+import org.semanticweb.owlapi.model.OWLOntology;
+import org.semanticweb.owlapi.model.OWLOntologyManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Command that runs a sparql query expecting zero results. Any results represent violations in the queried ontology
+ */
+public class VerifyCommand implements Command {
+
+    private static final Logger logger = LoggerFactory.getLogger(VerifyCommand.class);
+
+    @Override
+    public String getName() {
+        return "verify";
+    }
+
+    @Override
+    public String getDescription() {
+        return "Runs a sparql query on an ontology. Any results of the query are violations, counted, and reported";
+    }
+
+    @Override
+    public String getUsage() {
+        return "robot verify --input ONTOLOGY --queries FILE [FILE [...]] --report-dir DIR";
+    }
+
+    @Override
+    public Options getOptions() {
+        Options options = CommandLineHelper.getCommonOptions();
+        Option queries = new Option("q", "queries", true, "List of sparql queries to find violations");
+        queries.setRequired(true);
+        queries.setArgs(Option.UNLIMITED_VALUES);
+        options.addOption(queries);
+        Option report = new Option("O", "output-dir", true, "Directory to place reports in");
+        report.setRequired(true);
+        options.addOption(report);
+        options.addOption("i", "input", true, "Input Ontology");
+        return options;
+    }
+
+    @Override
+    public void main(String[] args) {
+        try {
+            execute(null, args);
+        } catch (Exception e) {
+            CommandLineHelper.handleException(getUsage(), getOptions(), e);
+        }
+    }
+
+    @Override
+    public CommandState execute(CommandState inputState, String[] args) throws Exception {
+
+        CommandLine line = CommandLineHelper.getCommandLine(getUsage(), getOptions(), args);
+
+        OWLOntologyManager ontologyManager = OWLManager.createOWLOntologyManager();
+        OWLOntology ontology = ontologyManager.loadOntologyFromOntologyDocument(new File(line.getOptionValue("input")));
+        DatasetGraph graph = QueryOperation.loadOntology(ontology);
+
+        File resultDir = new File(line.getOptionValue("output-dir"));
+
+        Map<File, Tuple<ResultSetRewindable, OutputStream>> resultMap = new HashMap<>();
+        for(String filePath : line.getOptionValues("queries")) {
+            File query = new File(filePath);
+            ResultSet results = QueryOperation.execQuery(graph, fileContents(query));
+            ResultSetRewindable resultsCopy = ResultSetFactory.copyResults(results);
+            File resultCsv = resultDir.toPath().resolve(FilenameUtils.getBaseName(filePath).concat(".csv")).toFile();
+            resultMap.put(query, new Tuple<>(resultsCopy, new FileOutputStream(resultCsv)));
+        }
+
+        boolean violationsExist = QueryOperation.execVerify(resultMap);
+        if(violationsExist) {
+            System.exit(1);
+        }
+
+        return inputState;
+    }
+
+    private static String fileContents(File file) {
+        try {
+            return Files.toString(file, Charset.defaultCharset());
+        } catch (IOException e) {
+            throw new CannotReadQuery("Cannot read from " + file + ": " + e.getMessage(), e);
+
+        }
+    }
+}
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 000b0d97..cccf7ec2 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/QueryOperation.java
@@ -1,19 +1,18 @@
 package org.obolibrary.robot;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileNotFoundException;
+import java.io.*;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
+import com.google.common.collect.Lists;
+import com.hp.hpl.jena.query.*;
 import com.hp.hpl.jena.rdf.model.Model;
+import org.apache.commons.io.FilenameUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.hp.hpl.jena.query.DatasetFactory;
-import com.hp.hpl.jena.query.QueryExecution;
-import com.hp.hpl.jena.query.QueryExecutionFactory;
-import com.hp.hpl.jena.query.ResultSet;
 import com.hp.hpl.jena.sparql.core.DatasetGraph;
 import com.hp.hpl.jena.sparql.core.DatasetGraphFactory;
 
@@ -85,6 +84,24 @@ public class QueryOperation {
         return exec.execConstruct();
     }
 
+    public static boolean execVerify(Map<File, Tuple<ResultSetRewindable, OutputStream>> queriesResults) throws IOException {
+        boolean isViolation = false;
+        for(File outFile : queriesResults.keySet()) {
+            Tuple<ResultSetRewindable, OutputStream> resultAndStream = queriesResults.get(outFile);
+            ResultSetRewindable results = resultAndStream.left();
+            OutputStream outStream = resultAndStream.right();
+            System.out.println("Rule " + outFile.getCanonicalPath() + ": " + results.size() + " violation(s)");
+            if(results.size() > 0) {
+                isViolation = true;
+                ResultSetMgr.write(System.err, results, Lang.CSV);
+                results.reset();
+            }
+            System.err.print('\n');
+            ResultSetMgr.write(outStream, results, Lang.CSV);
+        }
+        return isViolation;
+    }
+
     /**
      * Run a query and write the result to a file.
      *
diff --git a/robot-core/src/main/java/org/obolibrary/robot/Tuple.java b/robot-core/src/main/java/org/obolibrary/robot/Tuple.java
new file mode 100644
index 00000000..5e1202ec
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/Tuple.java
@@ -0,0 +1,121 @@
+package org.obolibrary.robot;
+
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * Created by edouglass on 8/14/17.
+ *
+ * Generic Tuple
+ */
+public class Tuple<A, B> implements Collection {
+
+    private A left;
+
+    private B right;
+
+    Tuple(A left, B right) {
+        this.left = left;
+        this.right = right;
+    }
+
+    public A left() {
+        return left;
+    }
+
+    public B right() {
+        return right;
+    }
+
+    @Override
+    public int size() {
+        return 2;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    @Override
+    public boolean contains(Object o) {
+        return o.equals(left) || o.equals(right);
+    }
+
+    @Override
+    public Iterator iterator() {
+        return Lists.newArrayList(left, right).iterator();
+    }
+
+    @Override
+    public Object[] toArray() {
+        return new Object[] {left, right};
+    }
+
+    @Override
+    public boolean add(Object o) {
+        return false;
+    }
+
+    @Override
+    public boolean remove(Object o) {
+        return false;
+    }
+
+    @Override
+    public boolean addAll(Collection c) {
+        return false;
+    }
+
+    @Override
+    public void clear() {
+        left = null;
+        right = null;
+    }
+
+    @Override
+    public boolean retainAll(Collection c) {
+        return Lists.newArrayList(left, right).retainAll(c);
+    }
+
+    @Override
+    public boolean removeAll(Collection c) {
+        return Lists.newArrayList(left, right).removeAll(c);
+    }
+
+    @Override
+    public boolean containsAll(Collection c) {
+        return Lists.newArrayList(left, right).containsAll(c);
+    }
+
+    @Override
+    public Object[] toArray(Object[] a) {
+        if(a.length < 2) {
+            return new Object[] {left, right};
+        } else {
+            a[0] = left;
+            a[1] = right;
+            return a;
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Tuple<?, ?> tuple = (Tuple<?, ?>) o;
+
+        if (!left.equals(tuple.left)) return false;
+        return right.equals(tuple.right);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = left.hashCode();
+        result = 211 * result + right.hashCode();
+        return result;
+    }
+}
diff --git a/robot-core/src/main/java/org/obolibrary/robot/exceptions/CannotReadQuery.java b/robot-core/src/main/java/org/obolibrary/robot/exceptions/CannotReadQuery.java
new file mode 100644
index 00000000..a536ad03
--- /dev/null
+++ b/robot-core/src/main/java/org/obolibrary/robot/exceptions/CannotReadQuery.java
@@ -0,0 +1,11 @@
+package org.obolibrary.robot.exceptions;
+
+
+/**
+ * Created by edouglass on 8/9/17.
+ */
+public class CannotReadQuery extends RuntimeException {
+    public CannotReadQuery(String s, Exception e) {
+        super(s, e);
+    }
+}


Test patch:
diff --git a/robot-core/src/test/java/org/obolibrary/robot/QueryOperationTest.java b/robot-core/src/test/java/org/obolibrary/robot/QueryOperationTest.java
index 69df1b29..3a453783 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/QueryOperationTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/QueryOperationTest.java
@@ -1,10 +1,16 @@
 package org.obolibrary.robot;
 
-import java.io.IOException;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.google.common.collect.Lists;
+import com.hp.hpl.jena.query.ResultSetFactory;
+import com.hp.hpl.jena.query.ResultSetRewindable;
 import org.junit.Test;
 
 import com.hp.hpl.jena.query.ResultSet;
@@ -14,6 +20,7 @@ import com.hp.hpl.jena.rdf.model.RDFNode;
 import com.hp.hpl.jena.rdf.model.Resource;
 import com.hp.hpl.jena.rdf.model.ResourceFactory;
 import com.hp.hpl.jena.sparql.core.DatasetGraph;
+import org.obolibrary.robot.exceptions.CannotReadQuery;
 import org.semanticweb.owlapi.model.OWLOntology;
 import org.semanticweb.owlapi.model.OWLOntologyCreationException;
 import org.semanticweb.owlapi.model.OWLOntologyStorageException;
@@ -78,4 +85,48 @@ public class QueryOperationTest extends CoreTest {
         assertTrue(model.contains(s, p, o));
     }
 
+    @Test
+    public void testExecVerifyWithViolations() throws IOException, OWLOntologyStorageException {
+
+        OWLOntology ontology = loadOntology("/simple.owl");
+        DatasetGraph graph = QueryOperation.loadOntology(ontology);
+        String allViolations =
+            "SELECT ?s ?p ?o\n" +
+                "WHERE {\n" +
+                "    ?s ?p ?o .\n" +
+                "}\n" +
+                "LIMIT 10";
+        ResultSet resultSet = QueryOperation.execQuery(graph, allViolations);
+        ResultSetRewindable copy = ResultSetFactory.copyResults(resultSet);
+        Map<File, Tuple<ResultSetRewindable, OutputStream>> testResults = new HashMap<>();
+        ByteArrayOutputStream testOut = new ByteArrayOutputStream();
+        testResults.put(new File("/path/to/rule.sparql"), new Tuple<>(copy, testOut));
+        boolean violations = QueryOperation.execVerify(testResults);
+
+        assertTrue(violations);
+        assertEquals(7, Lists.newArrayList(testOut.toString().split("\n")).size());
+    }
+
+    @Test
+    public void testExecVerifyNoViolations() throws IOException, OWLOntologyStorageException {
+        OWLOntology ontology = loadOntology("/simple.owl");
+        DatasetGraph graph = QueryOperation.loadOntology(ontology);
+        String allViolations =
+            "SELECT ?s ?p ?o\n" +
+                "WHERE {\n" +
+                "    \n" +
+                "}\n" +
+                "LIMIT 0";
+        ResultSet resultSet = QueryOperation.execQuery(graph, allViolations);
+        ResultSetRewindable copy = ResultSetFactory.copyResults(resultSet);
+        Map<File, Tuple<ResultSetRewindable, OutputStream>> testResults = new HashMap<>();
+        ByteArrayOutputStream testOut = new ByteArrayOutputStream();
+        testResults.put(new File("/path/to/rule.sparql"), new Tuple<>(copy, testOut));
+        boolean violations = QueryOperation.execVerify(testResults);
+
+        assertFalse(violations);
+        assertEquals(1, Lists.newArrayList(testOut.toString().split("\n")).size());
+    }
+
+
 }