665: Can't load JSON schemas with URN value in id field (#906)

* 665: Can't load JSON schemas with URN value in id field

* Better handling of URNs
diff --git a/pom.xml b/pom.xml
index c9ae09a..cb6966f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
 
   <groupId>com.networknt</groupId>
   <artifactId>json-schema-validator</artifactId>
-  <version>1.0.87</version>
+  <version>1.0.88</version>
   <packaging>bundle</packaging>
   <name>JsonSchemaValidator</name>
   <description>A json schema validator that supports draft v4, v6, v7, v2019-09 and v2020-12</description>
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 84d8c31..d5050ae 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -214,7 +214,7 @@
             if (node.isMissingNode()) {
                 node = handleNullNode(ref, schema);
             }
-        } else if (ref.startsWith("#") && ref.length() > 1) {
+        } else if ((ref.startsWith("#") && ref.length() > 1) || (ref.startsWith("urn:") && ref.length() > 4)) {
             node = this.metaSchema.getNodeByFragmentRef(ref, node);
             if (node == null) {
                 node = handleNullNode(ref, schema);
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index 77ce452..b6fbd21 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -59,6 +59,8 @@
             for (final String scheme : URLFactory.SUPPORTED_SCHEMES) {
                 this.uriFactoryMap.put(scheme, urlFactory);
             }
+            // Adds support for creating URNs.
+            this.uriFactoryMap.put(URNURIFactory.SCHEME, new URNURIFactory());
 
             // Adds support for fetching with {@link URL}s.
             final URIFetcher urlFetcher = new URLFetcher();
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index 356ffba..83e8afe 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -19,6 +19,7 @@
 import com.fasterxml.jackson.databind.JsonNode;

 import com.networknt.schema.CollectorContext.Scope;

 import com.networknt.schema.uri.URIFactory;

+import com.networknt.schema.uri.URNURIFactory;

 import com.networknt.schema.urn.URNFactory;

 import org.slf4j.Logger;

 import org.slf4j.LoggerFactory;

@@ -35,6 +36,7 @@
     private JsonSchema parentSchema;

 

     private static final String REF_CURRENT = "#";

+    private static final String URN_SCHEME = URNURIFactory.SCHEME;

 

     public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {

         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext);

@@ -78,6 +80,12 @@
                 if (schemaUri == null) {

                     return null;

                 }

+            } else if (URN_SCHEME.equals(schemaUri.getScheme())) {

+                // Try to resolve URN schema as a JsonSchemaRef to some sub-schema of the parent

+                JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, schemaUri.toString(), refValueOriginal);

+                if (ref != null) {

+                    return ref;

+                }

             }

 

             // This should retrieve schemas regardless of the protocol that is in the uri.

@@ -91,6 +99,13 @@
         if (refValue.equals(REF_CURRENT)) {

             return new JsonSchemaRef(parent.findAncestor());

         }

+        return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal);

+    }

+

+    private static JsonSchemaRef getJsonSchemaRef(JsonSchema parent,

+                                                  ValidationContext validationContext,

+                                                  String refValue,

+                                                  String refValueOriginal) {

         JsonNode node = parent.getRefSchemaNode(refValue);

         if (node != null) {

             JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal);

diff --git a/src/main/java/com/networknt/schema/uri/URLFactory.java b/src/main/java/com/networknt/schema/uri/URLFactory.java
index 5850a3b..61fa56d 100644
--- a/src/main/java/com/networknt/schema/uri/URLFactory.java
+++ b/src/main/java/com/networknt/schema/uri/URLFactory.java
@@ -30,7 +30,7 @@
  */
 public final class URLFactory implements URIFactory {
     // These supported schemes are defined in {@link #URL(String, String, int, String)}.
-    public static final Set<String> SUPPORTED_SCHEMES = Collections.unmodifiableSet(new HashSet<String>(
+    public static final Set<String> SUPPORTED_SCHEMES = Collections.unmodifiableSet(new HashSet<>(
             Arrays.asList("http", "https", "ftp", "file", "jar")));
 
     /**
diff --git a/src/main/java/com/networknt/schema/uri/URLFetcher.java b/src/main/java/com/networknt/schema/uri/URLFetcher.java
index 14899cf..d5bb8d9 100644
--- a/src/main/java/com/networknt/schema/uri/URLFetcher.java
+++ b/src/main/java/com/networknt/schema/uri/URLFetcher.java
@@ -22,7 +22,9 @@
 import java.net.URI;
 import java.net.URL;
 import java.net.URLConnection;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 
 /**
@@ -32,7 +34,7 @@
 
     // These supported schemes are defined in {@link #URL(String, String, int, String)}.
     // This fetcher also supports the {@link URL}s created with the {@link ClasspathURIFactory}.
-    public static final Set<String> SUPPORTED_SCHEMES = Collections.unmodifiableSet(URLFactory.SUPPORTED_SCHEMES);
+    public static final Set<String> SUPPORTED_SCHEMES = URLFactory.SUPPORTED_SCHEMES;
 
     /**
      * {@inheritDoc}
diff --git a/src/main/java/com/networknt/schema/uri/URNURIFactory.java b/src/main/java/com/networknt/schema/uri/URNURIFactory.java
new file mode 100644
index 0000000..8ea7914
--- /dev/null
+++ b/src/main/java/com/networknt/schema/uri/URNURIFactory.java
@@ -0,0 +1,30 @@
+package com.networknt.schema.uri;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * A URIFactory that handles "urn" scheme of {@link URI}s.
+ */
+public final class URNURIFactory implements URIFactory {
+
+    public static final String SCHEME = "urn";
+
+    @Override
+    public URI create(final String uri) {
+        try {
+            return URI.create(uri);
+        } catch (IllegalArgumentException e) {
+            throw new IllegalArgumentException("Unable to create URI.", e);
+        }
+    }
+
+    @Override
+    public URI create(final URI baseURI, final String segment) {
+        String urnPart = baseURI.getRawSchemeSpecificPart();
+        int pos = urnPart.indexOf(':');
+        String namespace = pos < 0 ? urnPart : urnPart.substring(0, pos);
+        return URI.create(SCHEME + ":" + namespace + ":" + segment);
+    }
+}
diff --git a/src/test/java/com/networknt/schema/Issue665Test.java b/src/test/java/com/networknt/schema/Issue665Test.java
new file mode 100644
index 0000000..1b9f8bd
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue665Test.java
@@ -0,0 +1,49 @@
+package com.networknt.schema;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Set;
+
+public class Issue665Test extends BaseJsonSchemaValidatorTest {
+
+    @Test
+    void testUrnUriAsLocalRef() throws IOException {
+        JsonSchema schema = getJsonSchemaFromClasspath("draft7/urn/issue665.json", SpecVersion.VersionFlag.V7);
+        Assertions.assertNotNull(schema);
+        Assertions.assertDoesNotThrow(schema::initializeValidators);
+        Set<ValidationMessage> messages = schema.validate(getJsonNodeFromStringContent(
+                "{\"myData\": {\"value\": \"hello\"}}"));
+        Assertions.assertEquals(messages, Collections.emptySet());
+    }
+
+    @Test
+    void testUrnUriAsLocalRef_ExternalURN() {
+        JsonSchemaFactory factory = JsonSchemaFactory
+                .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+                .uriFetcher(uri -> uri.equals(URI.create("urn:data"))
+                        ? Thread.currentThread().getContextClassLoader()
+                            .getResourceAsStream("draft7/urn/issue665_external_urn_subschema.json")
+                        : null,
+                        "urn")
+                .build();
+
+        try (InputStream is = Thread.currentThread().getContextClassLoader()
+                .getResourceAsStream("draft7/urn/issue665_external_urn_ref.json")) {
+            JsonSchema schema = factory.getSchema(is);
+            Assertions.assertNotNull(schema);
+            Assertions.assertDoesNotThrow(schema::initializeValidators);
+            Set<ValidationMessage> messages = schema.validate(getJsonNodeFromStringContent(
+                    "{\"myData\": {\"value\": \"hello\"}}"));
+            Assertions.assertEquals(messages, Collections.emptySet());
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+}
diff --git a/src/test/resources/draft7/urn/issue665.json b/src/test/resources/draft7/urn/issue665.json
new file mode 100644
index 0000000..57a3bf8
--- /dev/null
+++ b/src/test/resources/draft7/urn/issue665.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "type": "object",
+  "required": [
+    "myData"
+  ],
+  "properties": {
+    "myData": {
+      "$ref": "urn:data"
+    }
+  },
+  "definitions": {
+    "data": {
+      "$id": "urn:data",
+      "type": "object",
+      "required": [
+        "value"
+      ],
+      "properties": {
+        "value": {
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/resources/draft7/urn/issue665_external_urn_ref.json b/src/test/resources/draft7/urn/issue665_external_urn_ref.json
new file mode 100644
index 0000000..2929686
--- /dev/null
+++ b/src/test/resources/draft7/urn/issue665_external_urn_ref.json
@@ -0,0 +1,12 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "type": "object",
+  "required": [
+    "myData"
+  ],
+  "properties": {
+    "myData": {
+      "$ref": "urn:data"
+    }
+  }
+}
diff --git a/src/test/resources/draft7/urn/issue665_external_urn_subschema.json b/src/test/resources/draft7/urn/issue665_external_urn_subschema.json
new file mode 100644
index 0000000..026f879
--- /dev/null
+++ b/src/test/resources/draft7/urn/issue665_external_urn_subschema.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "urn:data",
+  "type": "object",
+  "required": [
+    "value"
+  ],
+  "properties": {
+    "value": {
+      "type": "string"
+    }
+  }
+}