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                 indexDataModels(path, content);
105             }
106             // JSON Schema
107             if (tree.has("$schema") && !tree.get("$schema").isNull()) {
108                 indexJsonSchema(tree, path, content);
109             }
110             // Avro
111             indexAvro(path, content, tree);
112         } catch (Exception e) {
113             // Must not be JSON or YAML...
114         }
115 
116         try {
117             indexProto(path, content);
118             return;
119         } catch (Exception e) {
120             // I guess it's not Protobuf.
121         }
122     }
123 
124     private void indexAvro(Path path, ContentHandle content, JsonNode parsed) {
125         // TODO: is namespace required for an Avro schema?
126         String ns = parsed.get("namespace").asText();
127         String name = parsed.get("name").asText();
128         String resourceName = ns != null ? ns + "." + name : name;
129         IndexedResource resource = new IndexedResource(path, ArtifactType.AVRO, resourceName, content);
130         this.index.add(resource);
131     }
132 
133     private void indexProto(Path path, ContentHandle content) {
134         ProtobufFile.toProtoFileElement(content.content());
135 
136         IndexedResource resource = new IndexedResource(path, ArtifactType.PROTOBUF, null, content);
137         this.index.add(resource);
138     }
139 
140     private void indexJsonSchema(JsonNode schema, Path path, ContentHandle content) {
141         String resourceName = null;
142         if (schema.has("$id")) {
143             resourceName = schema.get("$id").asText(null);
144         }
145         IndexedResource resource = new IndexedResource(path, ArtifactType.JSON, resourceName, content);
146         this.index.add(resource);
147     }
148 
149     private void indexDataModels(Path path, ContentHandle content) {
150         try {
151             // Determine content type based on file extension
152             String contentType = ContentTypes.APPLICATION_JSON;
153             String fileName = path.getFileName().toString().toLowerCase();
154             if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
155                 contentType = ContentTypes.APPLICATION_YAML;
156             }
157 
158             // Parse JSON or YAML content
159             TypedContent typedContent = TypedContent.create(content, contentType);
160             JsonNode node = ContentTypeUtil.parseJsonOrYaml(typedContent);
161             Document doc = Library.readDocument((ObjectNode) node);
162 
163             if (doc == null) {
164                 throw new UnsupportedOperationException("Content is not OpenAPI or AsyncAPI.");
165             }
166 
167             String type = ArtifactType.OPENAPI;
168             if (ModelTypeUtil.isAsyncApiModel(doc)) {
169                 type = ArtifactType.ASYNCAPI;
170             }
171 
172             IndexedResource resource = new IndexedResource(path, type, null, content);
173             this.index.add(resource);
174         } catch (IOException e) {
175             throw new RuntimeException("Failed to parse OpenAPI/AsyncAPI document", e);
176         }
177     }
178 
179 }