View Javadoc
1   package io.apicurio.registry.maven.refs;
2   
3   import com.fasterxml.jackson.databind.JsonNode;
4   import com.fasterxml.jackson.databind.ObjectMapper;
5   import com.fasterxml.jackson.databind.node.ObjectNode;
6   import io.apicurio.datamodels.Library;
7   import io.apicurio.datamodels.models.Document;
8   import io.apicurio.datamodels.util.ModelTypeUtil;
9   import io.apicurio.registry.content.ContentHandle;
10  import io.apicurio.registry.content.TypedContent;
11  import io.apicurio.registry.content.util.ContentTypeUtil;
12  import io.apicurio.registry.rest.client.models.VersionMetaData;
13  import io.apicurio.registry.types.ArtifactType;
14  import io.apicurio.registry.types.ContentTypes;
15  import io.apicurio.registry.utils.protobuf.schema.ProtobufFile;
16  
17  import java.io.IOException;
18  import java.nio.file.Path;
19  import java.util.HashSet;
20  import java.util.Set;
21  
22  /**
23   * An index of the files available when discovering references in an artifact. This index is typically
24   * populated by getting a list of all files in a directory, or zip file. The index maps a resource name (this
25   * will vary depending on the artifact type) to the content of the resource. For example, Avro schemas will
26   * have resource names based on the qualified name of the type they define. JSON Schemas will have resources
27   * names based on the name of the file. The intent of this index is to resolve an external reference found in
28   * an artifact to an actual piece of content (e.g. file) in the index. If it cannot be resolved, that would
29   * typically mean that there is a broken reference in the schema/design.
30   */
31  public class ReferenceIndex {
32  
33      private static final ObjectMapper mapper = new ObjectMapper();
34  
35      private Set<IndexedResource> index = new HashSet<>();
36      private Set<Path> schemaPaths = new HashSet<>();
37  
38      /**
39       * Constructor.
40       */
41      public ReferenceIndex() {
42      }
43  
44      /**
45       * Constructor.
46       * 
47       * @param schemaPath
48       */
49      public ReferenceIndex(Path schemaPath) {
50          this.schemaPaths.add(schemaPath);
51      }
52  
53      /**
54       * @param path
55       */
56      public void addSchemaPath(Path path) {
57          this.schemaPaths.add(path);
58      }
59  
60      /**
61       * Look up a resource in the index. Returns <code>null</code> if no resource with that name is found.
62       * 
63       * @param resourceName
64       * @param relativeToFile
65       */
66      public IndexedResource lookup(String resourceName, Path relativeToFile) {
67          return index.stream().filter(resource -> resource.matches(resourceName, relativeToFile, schemaPaths))
68                  .findFirst().orElse(null);
69      }
70  
71      /**
72       * Index an existing (remote) reference using a resource name and remote artifact metadata.
73       * 
74       * @param resourceName
75       * @param vmd
76       */
77      public void index(String resourceName, VersionMetaData vmd) {
78          IndexedResource res = new IndexedResource(null, null, resourceName, null);
79          res.setRegistration(vmd);
80          this.index.add(res);
81      }
82  
83      /**
84       * Index the given content. Indexing will parse the content and figure out its resource name and type.
85       *
86       * @param path
87       * @param content
88       */
89      public void index(Path path, ContentHandle content) {
90          try {
91              // Determine content type based on file extension
92              String contentType = ContentTypes.APPLICATION_JSON;
93              String fileName = path.getFileName().toString().toLowerCase();
94              if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
95                  contentType = ContentTypes.APPLICATION_YAML;
96              }
97  
98              // Parse JSON or YAML content
99              TypedContent typedContent = TypedContent.create(content, contentType);
100             JsonNode tree = ContentTypeUtil.parseJsonOrYaml(typedContent);
101 
102             // OpenAPI
103             if (tree.has("openapi") || tree.has("swagger") || tree.has("asyncapi")
104                     || tree.has("openrpc")) {
105                 indexDataModels(path, content);
106             }
107             // JSON Schema
108             if (tree.has("$schema") && !tree.get("$schema").isNull()) {
109                 indexJsonSchema(tree, path, content);
110             }
111             // Avro
112             indexAvro(path, content, tree);
113         } catch (Exception e) {
114             // Must not be JSON or YAML...
115         }
116 
117         try {
118             indexProto(path, content);
119             return;
120         } catch (Exception e) {
121             // I guess it's not Protobuf.
122         }
123     }
124 
125     private void indexAvro(Path path, ContentHandle content, JsonNode parsed) {
126         // TODO: is namespace required for an Avro schema?
127         String ns = parsed.get("namespace").asText();
128         String name = parsed.get("name").asText();
129         String resourceName = ns != null ? ns + "." + name : name;
130         IndexedResource resource = new IndexedResource(path, ArtifactType.AVRO, resourceName, content);
131         this.index.add(resource);
132     }
133 
134     private void indexProto(Path path, ContentHandle content) {
135         ProtobufFile.toProtoFileElement(content.content());
136 
137         IndexedResource resource = new IndexedResource(path, ArtifactType.PROTOBUF, null, content);
138         this.index.add(resource);
139     }
140 
141     private void indexJsonSchema(JsonNode schema, Path path, ContentHandle content) {
142         String resourceName = null;
143         if (schema.has("$id")) {
144             resourceName = schema.get("$id").asText(null);
145         }
146         IndexedResource resource = new IndexedResource(path, ArtifactType.JSON, resourceName, content);
147         this.index.add(resource);
148     }
149 
150     private void indexDataModels(Path path, ContentHandle content) {
151         try {
152             // Determine content type based on file extension
153             String contentType = ContentTypes.APPLICATION_JSON;
154             String fileName = path.getFileName().toString().toLowerCase();
155             if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
156                 contentType = ContentTypes.APPLICATION_YAML;
157             }
158 
159             // Parse JSON or YAML content
160             TypedContent typedContent = TypedContent.create(content, contentType);
161             JsonNode node = ContentTypeUtil.parseJsonOrYaml(typedContent);
162             Document doc = Library.readDocument((ObjectNode) node);
163 
164             if (doc == null) {
165                 throw new UnsupportedOperationException(
166                         "Content is not OpenAPI, AsyncAPI, or OpenRPC.");
167             }
168 
169             String type = ArtifactType.OPENAPI;
170             if (ModelTypeUtil.isAsyncApiModel(doc)) {
171                 type = ArtifactType.ASYNCAPI;
172             } else if (ModelTypeUtil.isOpenRpcModel(doc)) {
173                 type = ArtifactType.OPENRPC;
174             }
175 
176             IndexedResource resource = new IndexedResource(path, type, null, content);
177             this.index.add(resource);
178         } catch (IOException e) {
179             throw new RuntimeException("Failed to parse OpenAPI/AsyncAPI document", e);
180         }
181     }
182 
183 }