Repository: ontodev/robot (java)

Issue:
ROBOT should fail hard if CURIE prefix is unknown
- [ ] Seems right now that robot templates does not fail hard when a prefix is unknown - instead an IRI is created in the form of a CURIE.

- [ ] I was wishing to mention that as well in general - I did have some cases what playing with SPARQL for example that I accidentally introduced curies as entity ids, and it would be kind of great if I could know about this: Maybe I should make a ROBOT report check for that?

Pull request:
Add check for malformed IRI in IOHelper.createIRI
Resolves #880

- [ ] `docs/` have been added/updated
- [x] tests have been added/updated
- [x] `mvn verify` says all tests pass
- [x] `mvn site` says all JavaDocs correct
- [x] `CHANGELOG.md` has been updated

This is a bit more sane than trying to do a regex match for a valid URL. If the output IRI is just a CURIE, this will fail (e.g., undefined prefix when running `template`). If the output IRI has a space it it, this will also fail (which we were just checking for a space before).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b72bf362..ec9b011a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 - Add links to query documentation for default rules in [`report`] [#879]
+- Ability to restrict [`report`] to base ontology [#872]
 
 ### Fixed
 - Fix printing violations in [`report`] [#823]
@@ -18,6 +19,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fix behaviour of `--preserve-structure` when using internal or external axiom selectors for [`remove`] or [`filter`] [#816]
 - Fix value rendering for entities in [`report`] [#874]
 
+### Changed
+- Do not allow malformed IRIs to be returned by `IOHelper` [#882]
+
 ## [1.8.1] - 2021-01-27
 
 ### Fixed
@@ -256,7 +260,9 @@ First official release of ROBOT!
 [`template`]: http://robot.obolibrary.org/template
 [`validate`]: http://robot.obolibrary.org/validate
 
+[#882]: https://github.com/ontodev/robot/pull/882
 [#874]: https://github.com/ontodev/robot/pull/874
+[#872]: https://github.com/ontodev/robot/pull/872
 [#858]: https://github.com/ontodev/robot/pull/858
 [#850]: https://github.com/ontodev/robot/pull/850
 [#834]: https://github.com/ontodev/robot/pull/834
diff --git a/docs/examples/my-base-report.tsv b/docs/examples/my-base-report.tsv
new file mode 100644
index 00000000..97ab4390
--- /dev/null
+++ b/docs/examples/my-base-report.tsv
@@ -0,0 +1,14 @@
+Level	Rule Name	Subject	Property	Value
+ERROR	missing_ontology_description	https://github.com/ontodev/robot/examples/edit.owl	dc11:description	
+ERROR	missing_ontology_license	https://github.com/ontodev/robot/examples/edit.owl	dc:license	
+ERROR	missing_ontology_title	https://github.com/ontodev/robot/examples/edit.owl	dc11:title	
+WARN	annotation_whitespace	obo:UBPROP_0000007	IAO:0000232	Used to connect a class to an adjectival form of its label. For example, a class with label 'intestine' may have a relational adjective 'intestinal'. 
+WARN	duplicate_label_synonym	UBERON:0000949	oboInOwl:hasExactSynonym	endocrine system
+WARN	duplicate_label_synonym	UBERON:0001851	oboInOwl:hasExactSynonym	cortex
+WARN	missing_definition	UBERON:0009569	IAO:0000115	
+WARN	missing_definition	obo:UBPROP_0000007	IAO:0000115	
+WARN	duplicate_scoped_synonym	UBERON:0002368	oboInOwl:hasExactSynonym	ductless gland
+WARN	duplicate_scoped_synonym	UBERON:0002368	oboInOwl:hasRelatedSynonym	ductless gland
+WARN	duplicate_scoped_synonym	UBERON:0002369	oboInOwl:hasExactSynonym	glandula suprarenalis
+WARN	duplicate_scoped_synonym	UBERON:0002369	oboInOwl:hasRelatedSynonym	glandula suprarenalis
+INFO	missing_superclass	UBERON:0001062	rdfs:subClassOf	
diff --git a/docs/report.md b/docs/report.md
index b1d05019..4f4d446a 100644
--- a/docs/report.md
+++ b/docs/report.md
@@ -8,7 +8,7 @@
 4. [Queries](#queries)
 5. [Profiles (`--profile`)](#profiles)
 6. [Executing on Disk (`--tdb`)](#executing-on-disk)
-7. [Limiting Results (`--limit`)](#limiting-results)
+7. [Limiting Results (`--limit`, `--base-iri`)](#limiting-results)
 
 ## Overview
 
@@ -154,6 +154,22 @@ robot report --input edit.owl \
 
 This example will only include the first 10,000 results for each report query, meaning that some violation counts may be incomplete. Typically, you should not need to include a limit, but when working with large ontologies (using TDB), there may be hundreds of thousands of results.
 
+The results can also be restricted to entities following one or more URI prefixes.
+
+```
+robot report --input edit.owl \
+  --base-iri "http://purl.obolibrary.org/obo/UBERON_" \
+  --base-iri "https://github.com/ontodev/robot/examples/edit" \
+  --base-iri "http://purl.obolibrary.org/obo/UBPROP_" \
+  --output my-base-report.tsv
+```
+
+Note that if you want annotations on your ontology itself be checked, be sure to
+include a suitable URI prefix. For example, in the snippet above, we have
+included `https://github.com/ontodev/robot/examples/edit`, which ensures that
+ontology annotations on `https://github.com/ontodev/robot/examples/edit.owl` are
+included in the test results.
+
 ## Working with import closures
 
 By default the report command will operate over the specified ontology, ignoring the import chain. This means if your ontology uses a class, and the class label is asserted in an import, you will receive a missing label error.
diff --git a/robot-command/src/main/java/org/obolibrary/robot/ReportCommand.java b/robot-command/src/main/java/org/obolibrary/robot/ReportCommand.java
index 6c4d7aa3..91952a1e 100644
--- a/robot-command/src/main/java/org/obolibrary/robot/ReportCommand.java
+++ b/robot-command/src/main/java/org/obolibrary/robot/ReportCommand.java
@@ -1,6 +1,7 @@
 package org.obolibrary.robot;
 
 import com.google.common.collect.Lists;
+import java.util.List;
 import java.util.Map;
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Options;
@@ -32,6 +33,7 @@ public class ReportCommand implements Command {
     o.addOption("I", "input-iri", true, "load ontology from an IRI");
     o.addOption("o", "output", true, "save report to a file");
     o.addOption("p", "profile", true, "reporting rules and levels to use");
+    o.addOption(null, "base-iri", true, "specify a base namespace");
     o.addOption("f", "format", true, "save report in a given format (TSV or YAML)");
     o.addOption("F", "fail-on", true, "logging level to fail on");
     o.addOption("l", "labels", true, "if true, use labels for output");
@@ -133,6 +135,9 @@ public class ReportCommand implements Command {
       }
     }
 
+    List<String> baseNamespaces = CommandLineHelper.getBaseNamespaces(line, ioHelper);
+    baseNamespaces.forEach(ioHelper::addBaseNamespace);
+
     boolean success;
 
     boolean tdb = CommandLineHelper.getBooleanValue(line, "tdb", false);
diff --git a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
index 51787df0..9bbea714 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/IOHelper.java
@@ -13,9 +13,11 @@ import com.google.common.collect.Sets;
 import com.opencsv.*;
 import java.io.*;
 import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.util.*;
+import java.util.regex.Pattern;
 import java.util.zip.*;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.FilenameUtils;
@@ -928,10 +930,26 @@ public class IOHelper {
       return null;
     }
 
-    if (iri.toString().contains(" ")) {
-      // Invalid IRI
-      return null;
+    if (iri.toString().startsWith("urn:")) {
+      // A URN is not a valid URL, so we check it with regex
+      Pattern urnPattern =
+          Pattern.compile(
+              "^urn:[a-z0-9][a-z0-9-]{0,31}:([a-z0-9()+,\\-.:=@;$_!*']|%[0-9a-f]{2})+$",
+              Pattern.CASE_INSENSITIVE);
+      if (!urnPattern.matcher(iri.toString()).matches()) {
+        // Not a valid URN (RFC2141)
+        return null;
+      }
+    } else {
+      try {
+        // Check for malformed URL, e.g., a CURIE was returned or the value passed has spaces
+        new URL(iri.toString());
+      } catch (MalformedURLException e) {
+        // Invalid IRI
+        return null;
+      }
     }
+
     return iri;
   }
 
diff --git a/robot-core/src/main/java/org/obolibrary/robot/ReportOperation.java b/robot-core/src/main/java/org/obolibrary/robot/ReportOperation.java
index 65c5dc84..c2af4c0c 100644
--- a/robot-core/src/main/java/org/obolibrary/robot/ReportOperation.java
+++ b/robot-core/src/main/java/org/obolibrary/robot/ReportOperation.java
@@ -87,6 +87,7 @@ public class ReportOperation {
     options.put("labels", "false");
     options.put("format", null);
     options.put("profile", null);
+    options.put("base-iri", null);
     options.put("tdb", "false");
     options.put("tdb-directory", ".tdb");
     options.put("keep-tdb-mappings", "false");
@@ -940,6 +941,23 @@ public class ReportOperation {
         continue;
       }
 
+      if (!ioHelper.getBaseNamespaces().isEmpty()) {
+        boolean base = false;
+        for (String base_prefix : ioHelper.getBaseNamespaces()) {
+          if (entity.startsWith(base_prefix)) {
+            logger.info(
+                String.format(
+                    "Skipping entity '%s' from report because it follows the base-namespace.",
+                    entity));
+            base = true;
+            break;
+          }
+        }
+        if (!base) {
+          continue;
+        }
+      }
+
       Violation violation;
       try {
         OWLClass cls = dataFactory.getOWLClass(ioHelper.createIRI(entity));
diff --git a/robot-maven-plugin/pom.xml b/robot-maven-plugin/pom.xml
index 622ec7df..36f6f479 100644
--- a/robot-maven-plugin/pom.xml
+++ b/robot-maven-plugin/pom.xml
@@ -35,7 +35,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-plugin-plugin</artifactId>
-        <version>3.3</version>
+        <version>3.6.1</version>
       </plugin>
 
       <!-- Enforce Google Java Style -->


Test patch:
diff --git a/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java b/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java
index 7ee7b0e8..c3113814 100644
--- a/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java
+++ b/robot-core/src/test/java/org/obolibrary/robot/IOHelperTest.java
@@ -1,6 +1,7 @@
 package org.obolibrary.robot;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import com.github.jsonldjava.core.Context;
 import java.io.ByteArrayInputStream;
@@ -56,6 +57,32 @@ public class IOHelperTest extends CoreTest {
     assertEquals(inputContext, outputContext);
   }
 
+  /**
+   * Test creating a series of valid and invalid IRIs from strings.
+   *
+   * @throws IOException on problem creating IOHelper
+   */
+  @Test
+  public void testCreateIRI() throws IOException {
+    IOHelper ioh = new IOHelper();
+
+    // Valid IRIs
+    IRI iri = ioh.createIRI("OBI:0000070");
+    assertEquals(IRI.create("http://purl.obolibrary.org/obo/OBI_0000070"), iri);
+    iri = ioh.createIRI("http://purl.obolibrary.org/obo/obi.owl");
+    assertEquals(IRI.create("http://purl.obolibrary.org/obo/obi.owl"), iri);
+    iri = ioh.createIRI("urn:isbn:0451450523");
+    assertEquals(IRI.create("urn:isbn:0451450523"), iri);
+
+    // Invalid IRIs should always return null
+    iri = ioh.createIRI("EXAMPLE:0000070");
+    assertNull(iri);
+    iri = ioh.createIRI("urn:0451450523");
+    assertNull(iri);
+    iri = ioh.createIRI("This is an example definition.");
+    assertNull(iri);
+  }
+
   /**
    * Test loading JSON files.
    *