Repository: ontodev/robot
Pull request #461 (2019-04-10 22:36:21)

Use owl-diff library for diff operation. Add markdown and html diff output formats.
This addresses some of the issues in #432, especially "separate diff logic from diff rendering". It also includes a Markdown diff rendering, although currently different levels of granularity are not configurable.

Diff in this PR is implemented by calling out to functionality in my owl-diff library, which is also used in the owl-diff server. This diff is faster than the old version (I think the main slowdown in the old one was rendering of all axioms to string before performing comparisons). Also, label rendering in the simple output is done with a custom ShortFormProvider rather than regex operations.

This version passes the existing tests, although I made one small change to one test data file to correct what I believe to be an inconsistency in CURIE rendering in the previous code.


Patch:
diff --git a/docs/diff.md b/docs/diff.md
index 0a050240..f0ccdd4a 100644
--- a/docs/diff.md
+++ b/docs/diff.md
@@ -20,9 +20,9 @@ If `--output` is provided then a report will be written with any differences bet
       --right release.owl \
       --output results/release-diff.txt
 
-See <a href="/examples/release-diff.txt" target="_blank">`release-diff.txt`</a> for an example. In the output, 'Ontology 1' corresponds to your `--left` input and 'Ontology 2' corresponds to your `--right` input.
+See [release-diff.txt](/examples/release-diff.txt) for an example. In the output, 'Ontology 1' corresponds to your `--left` input and 'Ontology 2' corresponds to your `--right` input.
 
-The output is in OWL Manchester syntax, but you can include entity labels with `--labels true`.
+The default "plain" output is in OWL Functional syntax with IRIs. You can include entity labels with `--labels true`. In addition, Markdown and HTML diff formats (based on Manchester syntax) are available. You can select the desired format using the `--format` (or `-f`) option, with possible values `plain`, `pretty` (text with labels and CURIEs), `html`, or `markdown`.
 
 You can also compare ontologies by IRI with `--left-iri` and `--right-iri`. You may want to compare a local file to a release, in which case:
 <!-- DO NOT TEST -->
diff --git a/robot-command/src/main/java/org/obolibrary/robot/DiffCommand.java b/robot-command/src/main/java/org/obolibrary/robot/DiffCommand.java
index c5383484..e3095f81 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/DiffCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/DiffCommand.java
@@ -47,7 +47,10 @@ public class DiffCommand implements Command {
     o.addOption("r", "right", true, "load right ontology from file");
     o.addOption("R", "right-iri", true, "load right ontology from IRI");
     o.addOption("o", "output", true, "save results to file");
-    o.addOption(null, "labels", true, "if true, use labels in place of entity IRIs");
+    o.addOption(
+        null, "labels", true, "if true, append labels after entity IRIs in the text format output");
+    o.addOption(
+        "f", "format", true, "format for diff output: plain (default) | pretty | html | markdown");
     options = o;
   }
 
@@ -150,6 +153,7 @@ public class DiffCommand implements Command {
 
     Map<String, String> options = new HashMap<>();
     options.put("labels", CommandLineHelper.getDefaultValue(line, "labels", "false"));
+    options.put("format", CommandLineHelper.getDefaultValue(line, "format", "plain"));
 
     DiffOperation.compare(leftOntology, rightOntology, ioHelper, writer, options);
     writer.flush();
diff --git a/robot-core/pom.xml b/robot-core/pom.xml
index 63bb68e6..0b06b7a7 100644
--- a/robot-core/pom.xml
+++ b/robot-core/pom.xml
@@ -148,5 +148,10 @@
         </exclusion>
       </exclusions>
     </dependency>
+    <dependency>
+      <groupId>org.geneontology</groupId>
+      <artifactId>owl-diff_2.12</artifactId>
+      <version>1.1.1</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/robot-core/src/main/java/org/obolibrary/robot/DiffOperation.java b/robot-core/src/main/java/org/obolibrary/robot/DiffOperation.java
index 424cbbdf..e919433a 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/DiffOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/DiffOperation.java
@@ -5,7 +5,16 @@ import java.io.Writer;
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+import org.geneontology.owl.differ.Differ;
+import org.geneontology.owl.differ.render.BasicDiffRenderer;
+import org.geneontology.owl.differ.render.HTMLDiffRenderer;
+import org.geneontology.owl.differ.render.MarkdownGroupedDiffRenderer;
+import org.geneontology.owl.differ.shortform.DoubleShortFormProvider;
+import org.geneontology.owl.differ.shortform.OBOShortenerShortFormProvider;
+import org.semanticweb.owlapi.apibinding.OWLManager;
 import org.semanticweb.owlapi.model.*;
+import org.semanticweb.owlapi.util.AnnotationValueShortFormProvider;
 import org.semanticweb.owlapi.util.DefaultPrefixManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,8 +46,8 @@ public class DiffOperation {
   }
 
   /**
-   * Given two ontologies, compare their sets of axiom strings, returning true if they are identical
-   * and false otherwise.
+   * Given two ontologies, compare their sets of axioms, annotations, imports, and ontology IDs,
+   * returning true if they are identical and false otherwise.
    *
    * @param ontology1 the first ontology
    * @param ontology2 the second ontology
@@ -53,8 +62,8 @@ public class DiffOperation {
   }
 
   /**
-   * Given two ontologies and a Writer, get the differences between axiom strings and write then to
-   * the writer. The ontologies are not changed.
+   * Given two ontologies and a Writer, get their differences and write then to the writer. The
+   * ontologies are not changed.
    *
    * @param ontology1 the first ontology
    * @param ontology2 the second ontology
@@ -68,8 +77,8 @@ public class DiffOperation {
   }
 
   /**
-   * Given two ontologies, a Writer, and a set of diff options, get the differences between axiom
-   * strings and write then to the writer. The ontologies are not changed.
+   * Given two ontologies, a Writer, and a set of diff options, get their differences and write then
+   * to the writer. The ontologies are not changed.
    *
    * @param ontology1 the first ontology
    * @param ontology2 the second ontology
@@ -88,20 +97,15 @@ public class DiffOperation {
       throws IOException {
 
     boolean useLabels = OptionsHelper.optionIsTrue(options, "labels");
+    String format = OptionsHelper.getOption(options, "format", "plain");
+    format = format.toLowerCase();
+    if (useLabels && format.equals("plain")) {
+      format = "pretty";
+    }
 
-    Map<IRI, String> labels = OntologyHelper.getLabels(ontology1);
-    labels.putAll(OntologyHelper.getLabels(ontology2));
-
-    Set<String> strings1 = getAxiomStrings(ontology1);
-    Set<String> strings2 = getAxiomStrings(ontology2);
-    Set<String> sorted;
-
-    Set<String> strings1minus2 = new HashSet<>(strings1);
-    strings1minus2.removeAll(strings2);
-    Set<String> strings2minus1 = new HashSet<>(strings2);
-    strings2minus1.removeAll(strings1);
+    Differ.BasicDiff diff = Differ.diff(ontology1, ontology2);
 
-    if (strings1minus2.size() == 0 && strings2minus1.size() == 0) {
+    if (diff.isEmpty()) {
       if (writer != null) {
         writer.write("Ontologies are identical\n");
       }
@@ -112,35 +116,57 @@ public class DiffOperation {
       return false;
     }
 
-    writer.write(strings1minus2.size() + " axioms in Ontology 1 but not in Ontology 2:\n");
-    sorted = new TreeSet<>();
-    for (String axiom : strings1minus2) {
-      if (useLabels) {
-        sorted.add("- " + addLabels(ioHelper, labels, axiom) + "\n");
-      } else {
-        sorted.add("- " + axiom + "\n");
-      }
-    }
-    for (String axiom : sorted) {
-      writer.write(axiom);
+    OWLOntologySetProvider ontologyProvider =
+        new DualOntologySetProvider(
+            ontology1.getOWLOntologyManager(), ontology2.getOWLOntologyManager());
+
+    switch (format) {
+      case "plain":
+        writer.write(BasicDiffRenderer.renderPlain(diff));
+        break;
+      case "pretty":
+        DefaultPrefixManager pm = ioHelper.getPrefixManager();
+        AnnotationValueShortFormProvider labelProvider =
+            new AnnotationValueShortFormProvider(
+                ontologyProvider,
+                pm,
+                pm,
+                Collections.singletonList(OWLManager.getOWLDataFactory().getRDFSLabel()),
+                Collections.emptyMap());
+        OBOShortenerShortFormProvider iriProvider = new OBOShortenerShortFormProvider(pm);
+        DoubleShortFormProvider doubleProvider =
+            new DoubleShortFormProvider(iriProvider, labelProvider);
+        writer.write(BasicDiffRenderer.render(diff, doubleProvider));
+        break;
+      case "markdown":
+        Differ.GroupedDiff groupedForMarkdown = Differ.groupedDiff(diff);
+        writer.write(MarkdownGroupedDiffRenderer.render(groupedForMarkdown, ontologyProvider));
+        break;
+      case "html":
+        Differ.GroupedDiff groupedForHTML = Differ.groupedDiff(diff);
+        writer.write(HTMLDiffRenderer.render(groupedForHTML, ontologyProvider));
+        break;
+      default:
+        throw new IOException("Unknown diff format: " + format);
     }
 
-    writer.write("\n");
+    return false;
+  }
+
+  private static class DualOntologySetProvider implements OWLOntologySetProvider {
 
-    writer.write(strings2minus1.size() + " axioms in Ontology 2 but not in Ontology 1:\n");
-    sorted = new TreeSet<>();
-    for (String axiom : strings2minus1) {
-      if (useLabels) {
-        sorted.add("+ " + addLabels(ioHelper, labels, axiom) + "\n");
-      } else {
-        sorted.add("+ " + axiom + "\n");
-      }
-    }
-    for (String axiom : sorted) {
-      writer.write(axiom);
+    private final Set<OWLOntology> ontologies = new HashSet<>();
+
+    public DualOntologySetProvider(OWLOntologySetProvider left, OWLOntologySetProvider right) {
+      ontologies.addAll(left.getOntologies());
+      ontologies.addAll(right.getOntologies());
     }
 
-    return false;
+    @Nonnull
+    @Override
+    public Set<OWLOntology> getOntologies() {
+      return Collections.unmodifiableSet(ontologies);
+    }
   }
 
   /**
@@ -150,7 +176,10 @@ public class DiffOperation {
    * @param labels a map from IRIs to label strings
    * @param axiom a string representation of an OWLAxiom
    * @return a string with labels inserted next to IRIs
+   * @deprecated This functionality is now provided by the owl-diff library and no longer used to
+   *     format results in DiffOperation.
    */
+  @Deprecated
   public static String addLabels(Map<IRI, String> labels, String axiom) {
     Matcher matcher = iriPattern.matcher(axiom);
     StringBuffer sb = new StringBuffer();
@@ -178,7 +207,10 @@ public class DiffOperation {
    * @param labels a map from IRIs to label strings
    * @param axiom a string representation of an OWLAxiom
    * @return a string with labels inserted next to CURIEs
+   * @deprecated This functionality is now provided by the owl-diff library and no longer used to
+   *     format results in DiffOperation.
    */
+  @Deprecated
   public static String addLabels(IOHelper ioHelper, Map<IRI, String> labels, String axiom) {
     DefaultPrefixManager pm = ioHelper.getPrefixManager();
     Matcher matcher = iriPattern.matcher(axiom);
@@ -207,7 +239,9 @@ public class DiffOperation {
    *
    * @param ontology the ontology to use
    * @return a set of strings, one for each axiom in the ontology
+   * @deprecated This functionality is no longer used within DiffOperation.
    */
+  @Deprecated
   public static Set<String> getAxiomStrings(OWLOntology ontology) {
     Set<String> strings = new HashSet<>();
     strings.add(ontology.getOntologyID().toString());


Test patch:
diff --git a/robot-core/src/test/resources/simple.diff b/robot-core/src/test/resources/simple.diff
index 47242fa8..d4cf0508 100644
--- a/robot-core/src/test/resources/simple.diff
+++ b/robot-core/src/test/resources/simple.diff
@@ -1,4 +1,4 @@
 0 axioms in Ontology 1 but not in Ontology 2:
 
 1 axioms in Ontology 2 but not in Ontology 1:
-+ SubClassOf(<https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test1>[Test 1] owl:Thing)
++ SubClassOf(<https://github.com/ontodev/robot/robot-core/src/test/resources/simple.owl#test1>[Test 1] <owl:Thing>)