1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package io.wcm.siteapi.openapi.validator;
21
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.net.URL;
25 import java.nio.charset.StandardCharsets;
26 import java.util.Iterator;
27 import java.util.concurrent.ConcurrentHashMap;
28 import java.util.concurrent.ConcurrentMap;
29 import java.util.regex.Pattern;
30
31 import org.apache.commons.io.IOUtils;
32 import org.jetbrains.annotations.NotNull;
33 import org.jetbrains.annotations.Nullable;
34 import org.openapi4j.core.exception.ResolutionException;
35 import org.openapi4j.core.model.v3.OAI3;
36 import org.openapi4j.core.model.v3.OAI3Context;
37 import org.openapi4j.core.util.TreeUtil;
38 import org.openapi4j.core.validation.ValidationException;
39 import org.openapi4j.core.validation.ValidationResults.ValidationItem;
40 import org.openapi4j.parser.model.v3.OpenApi3;
41 import org.openapi4j.parser.validation.v3.OpenApi3Validator;
42 import org.openapi4j.schema.validator.ValidationContext;
43 import org.openapi4j.schema.validator.v3.SchemaValidator;
44
45 import com.fasterxml.jackson.databind.JsonNode;
46 import com.fasterxml.jackson.databind.node.MissingNode;
47
48
49
50
51
52 public final class OpenApiSpec {
53
54 private final URL url;
55 private final String version;
56 private final JsonNode rootNode;
57 private final ValidationContext<OAI3> validationContext;
58 private final ConcurrentMap<String, OpenApiSchemaValidator> validators = new ConcurrentHashMap<>();
59
60
61
62
63
64
65
66 public OpenApiSpec(@NotNull String path, @NotNull String version) {
67 this(toUrl(path), version);
68 }
69
70 private static URL toUrl(@NotNull String path) {
71 URL url = OpenApiSpec.class.getClassLoader().getResource(path);
72 if (url == null) {
73 throw new IllegalArgumentException("File not found in class path: " + path);
74 }
75 return url;
76 }
77
78
79
80
81
82
83
84 public OpenApiSpec(@NotNull URL url, @NotNull String version) {
85 this.url = url;
86 this.version = version;
87 try {
88 String specContent = readFileContent(url);
89 rootNode = TreeUtil.yaml.readTree(specContent);
90 OAI3Context apiContext = new OAI3Context(url, rootNode);
91 validationContext = new ValidationContext<>(apiContext);
92 validateSpec(apiContext, rootNode, url);
93 }
94 catch (IOException | ResolutionException ex) {
95 throw new SpecInvalidException("Unable to load specification " + url + ": " + ex.getMessage(), ex);
96 }
97 }
98
99
100
101
102
103
104
105 private static String readFileContent(@NotNull URL url) throws IOException {
106 try (InputStream is = url.openStream()) {
107 if (is == null) {
108 throw new IllegalArgumentException("File does not exist: " + url);
109 }
110 String json = IOUtils.toString(is, StandardCharsets.UTF_8);
111
112
113
114
115
116
117
118 return json.replace("\"{contentPath}", "\"/{contentPath}");
119 }
120 }
121
122
123
124
125
126
127
128 @SuppressWarnings("null")
129 private static void validateSpec(OAI3Context context, JsonNode rootNode, URL url) {
130 OpenApi3 api = TreeUtil.json.convertValue(rootNode, OpenApi3.class);
131 api.setContext(context);
132 try {
133 OpenApi3Validator.instance().validate(api);
134 }
135 catch (ValidationException ex) {
136
137 StringBuilder result = new StringBuilder();
138 result.append(ex.getMessage());
139 for (ValidationItem item : ex.results().items()) {
140 result.append("\n").append(item.toString());
141 }
142 throw new SpecInvalidException("Specification is invalid: " + url + " - " + result.toString(), ex);
143 }
144 }
145
146
147
148
149 public @NotNull URL getURL() {
150 return this.url;
151 }
152
153
154
155
156 public @NotNull String getVersion() {
157 return this.version;
158 }
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178 public @NotNull OpenApiSchemaValidator getSchemaValidator(@NotNull String suffix) {
179
180 return validators.computeIfAbsent(suffix, this::buildSchemaValidator);
181 }
182
183
184
185
186
187
188 private @NotNull OpenApiSchemaValidator buildSchemaValidator(@NotNull String suffix) {
189 JsonNode matchingPath = findMatchingPathNode(suffix);
190 if (matchingPath == null) {
191 throw new IllegalArgumentException("No matching path definition found for suffix: " + suffix);
192 }
193
194 String pointer = "/get/responses/200/content/application~1json/schema";
195 JsonNode schemaNode = matchingPath.at(pointer);
196 if (schemaNode == null || schemaNode instanceof MissingNode) {
197 throw new IllegalArgumentException("No matching JSON schema definition at: " + pointer + ", suffix: " + suffix);
198 }
199 SchemaValidator schemaValidator = new SchemaValidator(validationContext, null, schemaNode);
200 return new OpenApiSchemaValidator(suffix, schemaValidator);
201 }
202
203
204
205
206
207
208 private @Nullable JsonNode findMatchingPathNode(@NotNull String suffix) {
209 Pattern endsWithSuffixPattern = Pattern.compile("^.+/" + Pattern.quote(suffix) + ".json$");
210 JsonNode paths = rootNode.findValue("paths");
211 if (paths != null) {
212 Iterator<String> fieldNames = paths.fieldNames();
213 while (fieldNames.hasNext()) {
214 String path = fieldNames.next();
215 if (endsWithSuffixPattern.matcher(path).matches()) {
216 return paths.findValue(path);
217 }
218 }
219 }
220 return null;
221 }
222
223 @Override
224 public String toString() {
225 return this.url.toString();
226 }
227
228 }