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