• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2016 Network New Technologies Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.networknt.schema;
18 
19 import com.fasterxml.jackson.databind.JsonNode;
20 import com.networknt.schema.SpecVersion.VersionFlag;
21 import com.networknt.schema.utils.StringUtils;
22 import org.slf4j.Logger;
23 import org.slf4j.LoggerFactory;
24 
25 import java.lang.reflect.InvocationTargetException;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.Objects;
31 import java.util.function.Consumer;
32 
33 /**
34  * Represents a meta-schema which is uniquely identified by its IRI.
35  */
36 public class JsonMetaSchema {
37     private static final Logger logger = LoggerFactory.getLogger(JsonMetaSchema.class);
38 
39     /**
40      * Factory for creating a format keyword.
41      */
42     public interface FormatKeywordFactory {
43         /**
44          * Creates a format keyword.
45          *
46          * @param formats the formats
47          * @return the format keyword
48          */
newInstance(Map<String, Format> formats)49         FormatKeyword newInstance(Map<String, Format> formats);
50     }
51 
52     /**
53      * Builder for {@link JsonMetaSchema}.
54      */
55     public static class Builder {
56         private String iri;
57         private String idKeyword = "$id";
58         private VersionFlag specification = null;
59         private Map<String, Keyword> keywords = new HashMap<>();
60         private Map<String, Format> formats = new HashMap<>();
61         private Map<String, Boolean> vocabularies = new HashMap<>();
62         private FormatKeywordFactory formatKeywordFactory = null;
63         private VocabularyFactory vocabularyFactory = null;
64         private KeywordFactory unknownKeywordFactory = null;
65 
Builder(String iri)66         public Builder(String iri) {
67             this.iri = iri;
68         }
69 
createKeywordsMap(Map<String, Keyword> kwords, Map<String, Format> formats)70         private Map<String, Keyword> createKeywordsMap(Map<String, Keyword> kwords, Map<String, Format> formats) {
71             boolean formatKeywordPresent = false;
72             Map<String, Keyword> map = new HashMap<>();
73             for (Map.Entry<String, Keyword> type : kwords.entrySet()) {
74                 String keywordName = type.getKey();
75                 Keyword keyword = type.getValue();
76                 if (ValidatorTypeCode.FORMAT.getValue().equals(keywordName)) {
77                     if (!(keyword instanceof FormatKeyword) && !ValidatorTypeCode.FORMAT.equals(keyword)) {
78                         throw new IllegalArgumentException("Overriding the keyword 'format' is not supported. Use the formatKeywordFactory and extend the FormatKeyword.");
79                     }
80                     // Indicate that the format keyword needs to be created
81                     formatKeywordPresent = true;
82                 } else {
83                     map.put(keyword.getValue(), keyword);
84                 }
85             }
86             if (formatKeywordPresent) {
87                 final FormatKeyword formatKeyword = formatKeywordFactory != null ? formatKeywordFactory.newInstance(formats)
88                         : new FormatKeyword(formats);
89                 map.put(formatKeyword.getValue(), formatKeyword);
90             }
91             return map;
92         }
93 
94         /**
95          * Sets the format keyword factory.
96          *
97          * @param formatKeywordFactory the format keyword factory
98          * @return the builder
99          */
formatKeywordFactory(FormatKeywordFactory formatKeywordFactory)100         public Builder formatKeywordFactory(FormatKeywordFactory formatKeywordFactory) {
101             this.formatKeywordFactory = formatKeywordFactory;
102             return this;
103         }
104 
105         /**
106          * Sets the vocabulary factory for handling custom vocabularies.
107          *
108          * @param vocabularyFactory the factory
109          * @return the builder
110          */
vocabularyFactory(VocabularyFactory vocabularyFactory)111         public Builder vocabularyFactory(VocabularyFactory vocabularyFactory) {
112             this.vocabularyFactory = vocabularyFactory;
113             return this;
114         }
115 
116         /**
117          * Sets the keyword factory for handling unknown keywords.
118          *
119          * @param unknownKeywordFactory the factory
120          * @return the builder
121          */
unknownKeywordFactory(KeywordFactory unknownKeywordFactory)122         public Builder unknownKeywordFactory(KeywordFactory unknownKeywordFactory) {
123             this.unknownKeywordFactory = unknownKeywordFactory;
124             return this;
125         }
126 
127         /**
128          * Customize the formats.
129          *
130          * @param customizer the customizer
131          * @return the builder
132          */
formats(Consumer<Map<String, Format>> customizer)133         public Builder formats(Consumer<Map<String, Format>> customizer) {
134             customizer.accept(this.formats);
135             return this;
136         }
137 
138         /**
139          * Customize the keywords.
140          *
141          * @param customizer the customizer
142          * @return the builder
143          */
keywords(Consumer<Map<String, Keyword>> customizer)144         public Builder keywords(Consumer<Map<String, Keyword>> customizer) {
145             customizer.accept(this.keywords);
146             return this;
147         }
148 
149         /**
150          * Adds the keyword.
151          *
152          * @param keyword the keyword
153          * @return the builder
154          */
keyword(Keyword keyword)155         public Builder keyword(Keyword keyword) {
156             this.keywords.put(keyword.getValue(), keyword);
157             return this;
158         }
159 
160         /**
161          * Adds the keywords.
162          *
163          * @param keywords the keywords
164          * @return the builder
165          */
keywords(Collection<? extends Keyword> keywords)166         public Builder keywords(Collection<? extends Keyword> keywords) {
167             for (Keyword keyword : keywords) {
168                 this.keywords.put(keyword.getValue(), keyword);
169             }
170             return this;
171         }
172 
173         /**
174          * Adds the format.
175          *
176          * @param format the format
177          * @return the builder
178          */
format(Format format)179         public Builder format(Format format) {
180             this.formats.put(format.getName(), format);
181             return this;
182         }
183 
184         /**
185          * Adds the formats.
186          *
187          * @param formats the formats
188          * @return the builder
189          */
formats(Collection<? extends Format> formats)190         public Builder formats(Collection<? extends Format> formats) {
191             for (Format format : formats) {
192                 format(format);
193             }
194             return this;
195         }
196 
197         /**
198          * Adds a required vocabulary.
199          * <p>
200          * Note that an error will be raised if this vocabulary is unknown.
201          *
202          * @param vocabulary the vocabulary IRI
203          * @return the builder
204          */
vocabulary(String vocabulary)205         public Builder vocabulary(String vocabulary) {
206             return vocabulary(vocabulary, true);
207         }
208 
209         /**
210          * Adds a vocabulary.
211          *
212          * @param vocabulary the vocabulary IRI
213          * @param required   true indicates if the vocabulary is not recognized
214          *                   processing should stop
215          * @return the builder
216          */
vocabulary(String vocabulary, boolean required)217         public Builder vocabulary(String vocabulary, boolean required) {
218             this.vocabularies.put(vocabulary, required);
219             return this;
220         }
221 
222         /**
223          * Adds the vocabularies.
224          *
225          * @param vocabularies the vocabularies to add
226          * @return the builder
227          */
vocabularies(Map<String, Boolean> vocabularies)228         public Builder vocabularies(Map<String, Boolean> vocabularies) {
229             this.vocabularies.putAll(vocabularies);
230             return this;
231         }
232 
233         /**
234          * Customize the vocabularies.
235          *
236          * @param customizer the customizer
237          * @return the builder
238          */
vocabularies(Consumer<Map<String, Boolean>> customizer)239         public Builder vocabularies(Consumer<Map<String, Boolean>> customizer) {
240             customizer.accept(this.vocabularies);
241             return this;
242         }
243 
244         /**
245          * Sets the specification.
246          *
247          * @param specification the specification
248          * @return the builder
249          */
specification(VersionFlag specification)250         public Builder specification(VersionFlag specification) {
251             this.specification = specification;
252             return this;
253         }
254 
255         /**
256          * Sets the id keyword.
257          *
258          * @param idKeyword the id keyword
259          * @return the builder
260          */
idKeyword(String idKeyword)261         public Builder idKeyword(String idKeyword) {
262             this.idKeyword = idKeyword;
263             return this;
264         }
265 
build()266         public JsonMetaSchema build() {
267             // create builtin keywords with (custom) formats.
268             Map<String, Keyword> keywords = this.keywords;
269             if (this.specification != null) {
270                 if (this.specification.getVersionFlagValue() >= SpecVersion.VersionFlag.V201909.getVersionFlagValue()) {
271                     keywords = new HashMap<>(this.keywords);
272                     for(Entry<String, Boolean> entry : this.vocabularies.entrySet()) {
273                         Vocabulary vocabulary = null;
274                         String id = entry.getKey();
275                         if (this.vocabularyFactory != null) {
276                             vocabulary = this.vocabularyFactory.getVocabulary(id);
277                         }
278                         if (vocabulary == null) {
279                             vocabulary = Vocabularies.getVocabulary(id);
280                         }
281                         if (vocabulary != null) {
282                             for (Keyword keyword : vocabulary.getKeywords()) {
283                                 keywords.put(keyword.getValue(), keyword);
284                             }
285                         } else if (Boolean.TRUE.equals(entry.getValue())) {
286                             ValidationMessage validationMessage = ValidationMessage.builder()
287                                     .message("Meta-schema ''{1}'' has unknown required vocabulary ''{2}''")
288                                     .arguments(this.iri, id).build();
289                             throw new InvalidSchemaException(validationMessage);
290                         }
291                     }
292                 }
293             }
294             Map<String, Keyword> result = createKeywordsMap(keywords, this.formats);
295             return new JsonMetaSchema(this.iri, this.idKeyword, result, this.vocabularies, this.specification, this);
296         }
297 
298         @Deprecated
addKeyword(Keyword keyword)299         public Builder addKeyword(Keyword keyword) {
300             return keyword(keyword);
301         }
302 
303         @Deprecated
addKeywords(Collection<? extends Keyword> keywords)304         public Builder addKeywords(Collection<? extends Keyword> keywords) {
305             return keywords(keywords);
306         }
307 
308         @Deprecated
addFormat(Format format)309         public Builder addFormat(Format format) {
310             return format(format);
311         }
312 
313         @Deprecated
addFormats(Collection<? extends Format> formats)314         public Builder addFormats(Collection<? extends Format> formats) {
315             return formats(formats);
316         }
317     }
318 
319     private final String iri;
320     private final String idKeyword;
321     private final Map<String, Keyword> keywords;
322     private final Map<String, Boolean> vocabularies;
323     private final VersionFlag specification;
324 
325     private final Builder builder;
326 
JsonMetaSchema(String iri, String idKeyword, Map<String, Keyword> keywords, Map<String, Boolean> vocabularies, VersionFlag specification, Builder builder)327     JsonMetaSchema(String iri, String idKeyword, Map<String, Keyword> keywords, Map<String, Boolean> vocabularies, VersionFlag specification, Builder builder) {
328         if (StringUtils.isBlank(iri)) {
329             throw new IllegalArgumentException("iri must not be null or blank");
330         }
331         if (StringUtils.isBlank(idKeyword)) {
332             throw new IllegalArgumentException("idKeyword must not be null or blank");
333         }
334         if (keywords == null) {
335             throw new IllegalArgumentException("keywords must not be null ");
336         }
337 
338         this.iri = iri;
339         this.idKeyword = idKeyword;
340         this.keywords = keywords;
341         this.specification = specification;
342         this.vocabularies = vocabularies;
343         this.builder = builder;
344     }
345 
getV4()346     public static JsonMetaSchema getV4() {
347         return new Version4().getInstance();
348     }
349 
getV6()350     public static JsonMetaSchema getV6() {
351         return new Version6().getInstance();
352     }
353 
getV7()354     public static JsonMetaSchema getV7() {
355         return new Version7().getInstance();
356     }
357 
getV201909()358     public static JsonMetaSchema getV201909() {
359         return new Version201909().getInstance();
360     }
361 
getV202012()362     public static JsonMetaSchema getV202012() {
363         return new Version202012().getInstance();
364     }
365 
366     /**
367      * Create a builder without keywords or formats.
368      * <p>
369      * Use {@link #getV4()} for the Draft 4 Metaschema, or if you need a builder based on Draft4, use
370      *
371      * <code>
372      * JsonMetaSchema.builder("http://your-metaschema-iri", JsonSchemaFactory.getDraftV4()).build();
373      * </code>
374      *
375      * @param iri the IRI of the metaschema that will be defined via this builder.
376      * @return a builder instance without any keywords or formats - usually not what one needs.
377      */
builder(String iri)378     public static Builder builder(String iri) {
379         return new Builder(iri);
380     }
381 
382     /**
383      * Create a builder.
384      *
385      * @param iri       the IRI of your new JsonMetaSchema that will be defined via
386      *                  this builder.
387      * @param blueprint the JsonMetaSchema to base your custom JsonMetaSchema on.
388      * @return a builder instance preconfigured to be the same as blueprint, but
389      *         with a different uri.
390      */
builder(String iri, JsonMetaSchema blueprint)391     public static Builder builder(String iri, JsonMetaSchema blueprint) {
392         Builder builder = builder(blueprint);
393         builder.iri = iri;
394         return builder;
395     }
396 
397     /**
398      * Create a builder.
399      *
400      * @param blueprint the JsonMetaSchema to base your custom JsonMetaSchema on.
401      * @return a builder instance preconfigured to be the same as blueprint
402      */
builder(JsonMetaSchema blueprint)403     public static Builder builder(JsonMetaSchema blueprint) {
404         Map<String, Boolean> vocabularies = new HashMap<>(blueprint.getVocabularies());
405         return builder(blueprint.getIri())
406                 .idKeyword(blueprint.idKeyword)
407                 .keywords(blueprint.builder.keywords.values())
408                 .formats(blueprint.builder.formats.values())
409                 .specification(blueprint.getSpecification())
410                 .vocabularies(vocabularies)
411                 .vocabularyFactory(blueprint.builder.vocabularyFactory)
412                 .formatKeywordFactory(blueprint.builder.formatKeywordFactory)
413                 .unknownKeywordFactory(blueprint.builder.unknownKeywordFactory)
414                 ;
415     }
416 
getIdKeyword()417     public String getIdKeyword() {
418         return this.idKeyword;
419     }
420 
readId(JsonNode schemaNode)421     public String readId(JsonNode schemaNode) {
422         return readText(schemaNode, this.idKeyword);
423     }
424 
readAnchor(JsonNode schemaNode)425     public String readAnchor(JsonNode schemaNode) {
426         boolean supportsAnchor = this.keywords.containsKey("$anchor");
427         if (supportsAnchor) {
428             return readText(schemaNode, "$anchor");
429         }
430         return null;
431     }
432 
readDynamicAnchor(JsonNode schemaNode)433     public String readDynamicAnchor(JsonNode schemaNode) {
434         boolean supportsDynamicAnchor = this.keywords.containsKey("$dynamicAnchor");
435         if (supportsDynamicAnchor) {
436             return readText(schemaNode, "$dynamicAnchor");
437         }
438         return null;
439     }
440 
readText(JsonNode node, String field)441     private static String readText(JsonNode node, String field) {
442         JsonNode idNode = node.get(field);
443         if (idNode == null || !idNode.isTextual()) {
444             return null;
445         }
446         return idNode.textValue();
447     }
448 
getIri()449     public String getIri() {
450         return this.iri;
451     }
452 
getKeywords()453     public Map<String, Keyword> getKeywords() {
454         return this.keywords;
455     }
456 
getVocabularies()457     public Map<String, Boolean> getVocabularies() {
458         return this.vocabularies;
459     }
460 
getSpecification()461     public VersionFlag getSpecification() {
462         return this.specification;
463     }
464 
465     /**
466      * Creates a new validator of the keyword.
467      *
468      * @param validationContext the validation context
469      * @param schemaLocation the schema location
470      * @param evaluationPath the evaluation path
471      * @param keyword the keyword
472      * @param schemaNode the schema node
473      * @param parentSchema the parent schema
474      * @return the validator
475      */
newValidator(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword, JsonNode schemaNode, JsonSchema parentSchema)476     public JsonValidator newValidator(ValidationContext validationContext, SchemaLocation schemaLocation,
477             JsonNodePath evaluationPath, String keyword, JsonNode schemaNode, JsonSchema parentSchema) {
478         try {
479             Keyword kw = this.keywords.get(keyword);
480             if (kw == null) {
481                 if ("message".equals(keyword) && validationContext.getConfig().isCustomMessageSupported()) {
482                     return null;
483                 }
484                 if (ValidatorTypeCode.DISCRIMINATOR.getValue().equals(keyword)
485                         && validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
486                     return ValidatorTypeCode.DISCRIMINATOR.newValidator(schemaLocation, evaluationPath, schemaNode,
487                             parentSchema, validationContext);
488                 }
489                 kw = this.builder.unknownKeywordFactory != null
490                         ? this.builder.unknownKeywordFactory.getKeyword(keyword, validationContext)
491                         : UnknownKeywordFactory.getInstance().getKeyword(keyword, validationContext);
492                 if (kw == null) {
493                     return null;
494                 }
495             }
496             return kw.newValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext);
497         } catch (InvocationTargetException e) {
498             if (e.getTargetException() instanceof JsonSchemaException) {
499                 logger.error("Error:", e);
500                 throw (JsonSchemaException) e.getTargetException();
501             }
502             logger.warn("Could not load validator {}", keyword);
503             throw new JsonSchemaException(e.getTargetException());
504         } catch (JsonSchemaException e) {
505             throw e;
506         } catch (Exception e) {
507             logger.warn("Could not load validator {}", keyword);
508             throw new JsonSchemaException(e);
509         }
510     }
511 
512     @Override
toString()513     public String toString() {
514         return this.iri;
515     }
516 
517     @Override
hashCode()518     public int hashCode() {
519         return Objects.hash(iri);
520     }
521 
522     @Override
equals(Object obj)523     public boolean equals(Object obj) {
524         if (this == obj)
525             return true;
526         if (obj == null)
527             return false;
528         if (getClass() != obj.getClass())
529             return false;
530         JsonMetaSchema other = (JsonMetaSchema) obj;
531         return Objects.equals(iri, other.iri);
532     }
533 }
534