Repository: swagger-api/swagger-parser
Pull request #1883 (2023-02-15 14:37:12)

implement support for custom URL resolvers


Patch:
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/DereferencersFactory.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/DereferencersFactory.java
index 17eb5ea6..f8fdd401 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/DereferencersFactory.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/DereferencersFactory.java
@@ -47,7 +47,7 @@ public class DereferencersFactory {
             if (ext == null) {
                 LOGGER.error("failed to load extension {}", ext);
             } else {
-                instance.addDereferencer(ext);
+                getInstance().addDereferencer(ext);
                 LOGGER.debug("adding OpenAPIDereferencer: {}", ext);
             }
         }
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceUtils.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceUtils.java
index d35c395c..79f8542d 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceUtils.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceUtils.java
@@ -1,24 +1,13 @@
 package io.swagger.v3.parser.reference;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import io.swagger.v3.core.util.Json31;
-import io.swagger.v3.core.util.Yaml31;
-import io.swagger.v3.parser.core.models.AuthorizationValue;
-import io.swagger.v3.parser.util.ClasspathHelper;
-import io.swagger.v3.parser.util.RemoteUrl;
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 
-import java.io.FileInputStream;
-import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLDecoder;
-import java.util.List;
 import java.util.regex.Pattern;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
 public class ReferenceUtils {
 
     public static String toBaseURI(String uri) throws Exception
@@ -68,52 +57,6 @@ public class ReferenceUtils {
         return false;
     }
 
-    public static String readURI(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
-        URI resolved = new URI(absoluteUri);
-        if (StringUtils.isNotBlank(resolved.getScheme())) {
-            if (resolved.getScheme().startsWith("http")) {
-                return readHttp(absoluteUri, auths);
-            } else if (resolved.getScheme().startsWith("file")) {
-                return readFile(absoluteUri);
-            } else if (resolved.getScheme().startsWith("classpath")) {
-                return readClasspath(absoluteUri);
-            }
-        }
-        // If no matches exists, try file
-        String content = null;
-        try {
-            content = readFile(absoluteUri);
-        } catch (Exception e) {
-            //
-        }
-        if (StringUtils.isBlank(content)) {
-            content = readClasspath(absoluteUri);
-        }
-        return content;
-    }
-
-    public static JsonNode deserializeIntoTree(String content) throws Exception {
-        boolean isJson = content.trim().startsWith("{");
-        return isJson ? Json31.mapper().readTree(content) : Yaml31.mapper().readTree(content);
-    }
-
-    public static JsonNode parse(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
-        return deserializeIntoTree(readURI(absoluteUri, auths));
-    }
-
-    public static String readFile(String uri) throws Exception {
-        try (InputStream inputStream = new FileInputStream(uri)) {
-            return IOUtils.toString(inputStream, UTF_8);
-        }
-    }
-
-    public static String readClasspath(String uri) throws Exception {
-        return ClasspathHelper.loadFileFromClasspath(uri);
-    }
-    public static String readHttp(String uri, List<AuthorizationValue> auths) throws Exception {
-        return RemoteUrl.urlToString(uri, auths);
-    }
-
     public static String unescapePointer(String jsonPathElement) {
         // URL decode the fragment
         try {
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
index 555babbd..9b9a6561 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/ReferenceVisitor.java
@@ -2,6 +2,8 @@ package io.swagger.v3.parser.reference;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
+import io.swagger.v3.core.util.Json31;
+import io.swagger.v3.core.util.Yaml31;
 import io.swagger.v3.oas.models.PathItem;
 import io.swagger.v3.oas.models.examples.Example;
 import io.swagger.v3.oas.models.headers.Header;
@@ -11,6 +13,7 @@ import io.swagger.v3.oas.models.parameters.Parameter;
 import io.swagger.v3.oas.models.parameters.RequestBody;
 import io.swagger.v3.oas.models.responses.ApiResponse;
 import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.parser.core.models.AuthorizationValue;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.LoggerFactory;
 
@@ -50,7 +53,7 @@ public class ReferenceVisitor extends AbstractVisitor {
         if (referenceSet.containsKey(baseUri)) {
             return referenceSet.get(baseUri);
         }
-        JsonNode node = ReferenceUtils.parse(baseUri, this.reference.getAuths());
+        JsonNode node = parse(baseUri, this.reference.getAuths());
 
         Reference ref = new Reference()
                 .auths(this.reference.getAuths())
@@ -205,11 +208,11 @@ public class ReferenceVisitor extends AbstractVisitor {
             else {
                 JsonNode node = null;
                 try {
-                    node = ReferenceUtils.parse(baseURI, this.reference.getAuths());
+                    node = parse(baseURI, this.reference.getAuths());
                 } catch (Exception e) {
                     // we can not parse, try ref
                     baseURI = toBaseURI(ref);
-                    node = ReferenceUtils.parse(baseURI, this.reference.getAuths());
+                    node = parse(baseURI, this.reference.getAuths());
                 }
                 reference = toSchemaReference(baseURI, node);
             }
@@ -266,4 +269,13 @@ public class ReferenceVisitor extends AbstractVisitor {
 
         return null;
     }
+
+    public JsonNode deserializeIntoTree(String content) throws Exception {
+        boolean isJson = content.trim().startsWith("{");
+        return isJson ? Json31.mapper().readTree(content) : Yaml31.mapper().readTree(content);
+    }
+
+    public JsonNode parse(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
+        return deserializeIntoTree(readURI(absoluteUri, auths));
+    }
 }
diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/Visitor.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/Visitor.java
index 651d18f7..d41b21cc 100644
--- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/Visitor.java
+++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/reference/Visitor.java
@@ -16,9 +16,19 @@ import io.swagger.v3.oas.models.parameters.RequestBody;
 import io.swagger.v3.oas.models.responses.ApiResponse;
 import io.swagger.v3.oas.models.responses.ApiResponses;
 import io.swagger.v3.oas.models.security.SecurityScheme;
-
+import io.swagger.v3.parser.core.models.AuthorizationValue;
+import io.swagger.v3.parser.util.ClasspathHelper;
+import io.swagger.v3.parser.util.RemoteUrl;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
 import java.util.List;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 public interface Visitor {
     OpenAPI visitOpenApi(OpenAPI openAPI);
 
@@ -52,4 +62,41 @@ public interface Visitor {
 
     Example visitExample(Example example);
 
+    default String readFile(String uri) throws Exception {
+        try (InputStream inputStream = new FileInputStream(uri)) {
+            return IOUtils.toString(inputStream, UTF_8);
+        }
+    }
+
+    default String readClasspath(String uri) throws Exception {
+        return ClasspathHelper.loadFileFromClasspath(uri);
+    }
+    default String readHttp(String uri, List<AuthorizationValue> auths) throws Exception {
+        return RemoteUrl.urlToString(uri, auths);
+    }
+
+    default String readURI(String absoluteUri, List<AuthorizationValue> auths) throws Exception {
+        URI resolved = new URI(absoluteUri);
+        if (StringUtils.isNotBlank(resolved.getScheme())) {
+            if (resolved.getScheme().startsWith("http")) {
+                return readHttp(absoluteUri, auths);
+            } else if (resolved.getScheme().startsWith("file")) {
+                return readFile(absoluteUri);
+            } else if (resolved.getScheme().startsWith("classpath")) {
+                return readClasspath(absoluteUri);
+            }
+        }
+        // If no matches exists, try file
+        String content = null;
+        try {
+            content = readFile(absoluteUri);
+        } catch (Exception e) {
+            //
+        }
+        if (StringUtils.isBlank(content)) {
+            content = readClasspath(absoluteUri);
+        }
+        return content;
+    }
+
 }


Test patch:
diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/CustomOpenAPIDereferencer.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/CustomOpenAPIDereferencer.java
new file mode 100644
index 00000000..1482cf25
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/CustomOpenAPIDereferencer.java
@@ -0,0 +1,51 @@
+package io.swagger.v3.parser.test;
+
+import io.swagger.v3.parser.core.models.AuthorizationValue;
+import io.swagger.v3.parser.reference.DereferencerContext;
+import io.swagger.v3.parser.reference.OpenAPI31Traverser;
+import io.swagger.v3.parser.reference.OpenAPIDereferencer31;
+import io.swagger.v3.parser.reference.Reference;
+import io.swagger.v3.parser.reference.ReferenceVisitor;
+import io.swagger.v3.parser.reference.Traverser;
+import io.swagger.v3.parser.reference.Visitor;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+public class CustomOpenAPIDereferencer extends OpenAPIDereferencer31 {
+    @Override
+    public Visitor buildReferenceVisitor(DereferencerContext context, Reference reference, Traverser traverser) {
+        return new CustomVisitor(reference, (OpenAPI31Traverser)traverser, new HashSet<>(), new HashMap<>());
+    }
+
+    static public class CustomVisitor extends ReferenceVisitor {
+
+        public CustomVisitor(Reference reference, OpenAPI31Traverser openAPITraverser, HashSet<Object> visited,
+                             HashMap<Object, Object> visitedMap) {
+            super(reference, openAPITraverser, visited, visitedMap);
+        }
+
+        @Override
+        public String readHttp(String uri, List<AuthorizationValue> auths) throws Exception {
+
+            if (uri.startsWith("http://example.com/custom")) {
+                return "openapi: 3.1.0\n" +
+                        "info:\n" +
+                        "  title: Domain\n" +
+                        "  version: '1.0'\n" +
+                        "components:\n" +
+                        "  pathItems:\n" +
+                        "    ExternalRef:\n" +
+                        "      get:\n" +
+                        "        description: ExternalRef domain\n" +
+                        "        operationId: ExternalRef PathItem\n" +
+                        "        responses:\n" +
+                        "          '200':\n" +
+                        "            description: OK";
+            } else {
+                return super.readHttp(uri, auths);
+            }
+        }
+    }
+}
diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV31ParserFSFullTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV31ParserFSFullTest.java
index 88d302f0..31248ad2 100644
--- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV31ParserFSFullTest.java
+++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV31ParserFSFullTest.java
@@ -19,4 +19,13 @@ public class OpenAPIV31ParserFSFullTest {
         SwaggerParseResult swaggerParseResult = new OpenAPIV3Parser().readLocation(uri, null, p);
         org.testng.Assert.assertEquals(Yaml31.pretty(swaggerParseResult.getOpenAPI()), FileUtils.readFileToString(new File("src/test/resources/3.1.0/dereference/fullFS/dereferenced.yaml")));
     }
+
+    @Test
+    public void testCustomUrlResolver() throws Exception {
+        ParseOptions p = new ParseOptions();
+        p.setResolve(true);
+        String uri = "3.1.0/dereference/custom/root.json";
+        SwaggerParseResult swaggerParseResult = new OpenAPIV3Parser().readLocation(uri, null, p);
+        org.testng.Assert.assertEquals(Yaml31.pretty(swaggerParseResult.getOpenAPI()), FileUtils.readFileToString(new File("src/test/resources/3.1.0/dereference/custom/dereferenced.yaml")));
+    }
 }
diff --git a/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/dereferenced.yaml b/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/dereferenced.yaml
new file mode 100644
index 00000000..aff2c3cf
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/dereferenced.yaml
@@ -0,0 +1,11 @@
+openapi: 3.1.0
+servers:
+- url: /
+paths:
+  /externalref:
+    get:
+      description: ExternalRef domain
+      operationId: ExternalRef PathItem
+      responses:
+        "200":
+          description: OK
diff --git a/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/root.json b/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/root.json
new file mode 100644
index 00000000..a966dbaf
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/3.1.0/dereference/custom/root.json
@@ -0,0 +1,8 @@
+{
+    "openapi": "3.1.0",
+    "paths": {
+        "/externalref": {
+            "$ref": "http://example.com/custom/nested/domain.yaml#/components/pathItems/ExternalRef"
+        }
+    }
+}
diff --git a/modules/swagger-parser-v3/src/test/resources/META-INF/services/io.swagger.v3.parser.reference.OpenAPIDereferencer b/modules/swagger-parser-v3/src/test/resources/META-INF/services/io.swagger.v3.parser.reference.OpenAPIDereferencer
new file mode 100644
index 00000000..8a5323b9
--- /dev/null
+++ b/modules/swagger-parser-v3/src/test/resources/META-INF/services/io.swagger.v3.parser.reference.OpenAPIDereferencer
@@ -0,0 +1,1 @@
+io.swagger.v3.parser.test.CustomOpenAPIDereferencer