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.fasterxml.jackson.databind.node.ObjectNode; 21 import com.networknt.schema.annotation.JsonNodeAnnotation; 22 import com.networknt.schema.i18n.DefaultMessageSource; 23 24 import org.slf4j.Logger; 25 26 import java.util.Collection; 27 import java.util.Iterator; 28 import java.util.Map; 29 import java.util.Set; 30 import java.util.function.Consumer; 31 32 public abstract class BaseJsonValidator extends ValidationMessageHandler implements JsonValidator { 33 protected final boolean suppressSubSchemaRetrieval; 34 35 protected final JsonNode schemaNode; 36 37 protected ValidationContext validationContext; 38 BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext)39 public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, 40 JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) { 41 this(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType, validatorType, validationContext, 42 false); 43 } 44 BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, ValidationContext validationContext, boolean suppressSubSchemaRetrieval)45 public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, 46 JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, 47 ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { 48 super(errorMessageType, 49 (validationContext != null && validationContext.getConfig() != null) 50 ? validationContext.getConfig().isCustomMessageSupported() 51 : true, 52 (validationContext != null && validationContext.getConfig() != null) 53 ? validationContext.getConfig().getMessageSource() 54 : DefaultMessageSource.getInstance(), 55 keyword, 56 parentSchema, schemaLocation, evaluationPath); 57 this.validationContext = validationContext; 58 this.schemaNode = schemaNode; 59 this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; 60 } 61 62 /** 63 * Copy constructor. 64 * 65 * @param copy to copy from 66 */ BaseJsonValidator(BaseJsonValidator copy)67 protected BaseJsonValidator(BaseJsonValidator copy) { 68 super(copy); 69 this.suppressSubSchemaRetrieval = copy.suppressSubSchemaRetrieval; 70 this.schemaNode = copy.schemaNode; 71 this.validationContext = copy.validationContext; 72 } 73 obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext)74 private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) { 75 final JsonNode node = schemaNode.get("id"); 76 77 if (node == null) { 78 return null; 79 } 80 81 if (node.equals(schemaNode.get("$schema"))) { 82 return null; 83 } 84 85 final String text = node.textValue(); 86 if (text == null) { 87 return null; 88 } 89 90 final SchemaLocation schemaLocation = SchemaLocation.of(node.textValue()); 91 92 return validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); 93 } 94 equals(double n1, double n2)95 protected static boolean equals(double n1, double n2) { 96 return Math.abs(n1 - n2) < 1e-12; 97 } 98 debug(Logger logger, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation)99 protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { 100 logger.debug("validate( {}, {}, {})", node, rootNode, instanceLocation); 101 } 102 103 /** 104 * Checks based on the current {@link DiscriminatorContext} whether the provided {@link JsonSchema} a match against 105 * against the current discriminator. 106 * 107 * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} 108 * @param discriminator the discriminator to use for the check 109 * @param discriminatorPropertyValue the value of the <code>discriminator/propertyName</code> field 110 * @param jsonSchema the {@link JsonSchema} to check 111 */ checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext, final ObjectNode discriminator, final String discriminatorPropertyValue, final JsonSchema jsonSchema)112 protected static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext, 113 final ObjectNode discriminator, 114 final String discriminatorPropertyValue, 115 final JsonSchema jsonSchema) { 116 if (discriminatorPropertyValue == null) { 117 currentDiscriminatorContext.markIgnore(); 118 return; 119 } 120 121 final JsonNode discriminatorMapping = discriminator.get("mapping"); 122 if (null == discriminatorMapping) { 123 checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, 124 discriminatorPropertyValue, 125 jsonSchema); 126 } else { 127 checkForExplicitDiscriminatorMappingMatch(currentDiscriminatorContext, 128 discriminatorPropertyValue, 129 discriminatorMapping, 130 jsonSchema); 131 if (!currentDiscriminatorContext.isDiscriminatorMatchFound() 132 && noExplicitDiscriminatorKeyOverride(discriminatorMapping, jsonSchema)) { 133 checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, 134 discriminatorPropertyValue, 135 jsonSchema); 136 } 137 } 138 } 139 140 /** 141 * Rolls up all nested and compatible discriminators to the root discriminator of the type. Detects attempts to redefine 142 * the <code>propertyName</code> or mappings. 143 * 144 * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} 145 * @param discriminator the discriminator to use for the check 146 * @param schema the value of the <code>discriminator/propertyName</code> field 147 * @param instanceLocation the logging prefix 148 */ registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext, final ObjectNode discriminator, final JsonSchema schema, final JsonNodePath instanceLocation)149 protected static void registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext, 150 final ObjectNode discriminator, 151 final JsonSchema schema, 152 final JsonNodePath instanceLocation) { 153 final JsonNode discriminatorOnSchema = schema.schemaNode.get("discriminator"); 154 if (null != discriminatorOnSchema && null != currentDiscriminatorContext 155 .getDiscriminatorForPath(schema.schemaLocation)) { 156 // this is where A -> B -> C inheritance exists, A has the root discriminator and B adds to the mapping 157 final JsonNode propertyName = discriminatorOnSchema.get("propertyName"); 158 if (null != propertyName) { 159 throw new JsonSchemaException(instanceLocation + " schema " + schema + " attempts redefining the discriminator property"); 160 } 161 final ObjectNode mappingOnContextDiscriminator = (ObjectNode) discriminator.get("mapping"); 162 final ObjectNode mappingOnCurrentSchemaDiscriminator = (ObjectNode) discriminatorOnSchema.get("mapping"); 163 if (null == mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { 164 // here we have a mapping on a nested discriminator and none on the root discriminator, so we can simply 165 // make it the root's 166 discriminator.set("mapping", discriminatorOnSchema); 167 } else if (null != mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { 168 // here we have to merge. The spec doesn't specify anything on this, but here we don't accept redefinition of 169 // mappings that already exist 170 final Iterator<Map.Entry<String, JsonNode>> fieldsToAdd = mappingOnCurrentSchemaDiscriminator.fields(); 171 while (fieldsToAdd.hasNext()) { 172 final Map.Entry<String, JsonNode> fieldToAdd = fieldsToAdd.next(); 173 final String mappingKeyToAdd = fieldToAdd.getKey(); 174 final JsonNode mappingValueToAdd = fieldToAdd.getValue(); 175 176 final JsonNode currentMappingValue = mappingOnContextDiscriminator.get(mappingKeyToAdd); 177 if (null != currentMappingValue && currentMappingValue != mappingValueToAdd) { 178 throw new JsonSchemaException(instanceLocation + "discriminator mapping redefinition from " + mappingKeyToAdd 179 + "/" + currentMappingValue + " to " + mappingValueToAdd); 180 } else if (null == currentMappingValue) { 181 mappingOnContextDiscriminator.set(mappingKeyToAdd, mappingValueToAdd); 182 } 183 } 184 } 185 } 186 currentDiscriminatorContext.registerDiscriminator(schema.schemaLocation, discriminator); 187 } 188 checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, final String discriminatorPropertyValue, final JsonSchema schema)189 private static void checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, 190 final String discriminatorPropertyValue, 191 final JsonSchema schema) { 192 if (schema.schemaLocation.getFragment().getName(-1).equals(discriminatorPropertyValue)) { 193 currentDiscriminatorContext.markMatch(); 194 } 195 } 196 checkForExplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, final String discriminatorPropertyValue, final JsonNode discriminatorMapping, final JsonSchema schema)197 private static void checkForExplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, 198 final String discriminatorPropertyValue, 199 final JsonNode discriminatorMapping, 200 final JsonSchema schema) { 201 final Iterator<Map.Entry<String, JsonNode>> explicitMappings = discriminatorMapping.fields(); 202 while (explicitMappings.hasNext()) { 203 final Map.Entry<String, JsonNode> candidateExplicitMapping = explicitMappings.next(); 204 if (candidateExplicitMapping.getKey().equals(discriminatorPropertyValue) 205 && ("#" + schema.schemaLocation.getFragment().toString()) 206 .equals(candidateExplicitMapping.getValue().asText())) { 207 currentDiscriminatorContext.markMatch(); 208 break; 209 } 210 } 211 } 212 noExplicitDiscriminatorKeyOverride(final JsonNode discriminatorMapping, final JsonSchema parentSchema)213 private static boolean noExplicitDiscriminatorKeyOverride(final JsonNode discriminatorMapping, 214 final JsonSchema parentSchema) { 215 final Iterator<Map.Entry<String, JsonNode>> explicitMappings = discriminatorMapping.fields(); 216 while (explicitMappings.hasNext()) { 217 final Map.Entry<String, JsonNode> candidateExplicitMapping = explicitMappings.next(); 218 if (candidateExplicitMapping.getValue().asText() 219 .equals(parentSchema.schemaLocation.getFragment().toString())) { 220 return false; 221 } 222 } 223 return true; 224 } 225 226 @Override getSchemaLocation()227 public SchemaLocation getSchemaLocation() { 228 return this.schemaLocation; 229 } 230 231 @Override getEvaluationPath()232 public JsonNodePath getEvaluationPath() { 233 return this.evaluationPath; 234 } 235 236 @Override getKeyword()237 public String getKeyword() { 238 return this.keyword.getValue(); 239 } 240 getSchemaNode()241 public JsonNode getSchemaNode() { 242 return this.schemaNode; 243 } 244 245 /** 246 * Gets the parent schema. 247 * <p> 248 * This is the lexical parent schema. 249 * 250 * @return the parent schema 251 */ getParentSchema()252 public JsonSchema getParentSchema() { 253 return this.parentSchema; 254 } 255 256 /** 257 * Gets the evaluation parent schema. 258 * <p> 259 * This is the dynamic parent schema when following references. 260 * 261 * @see JsonSchema#fromRef(JsonSchema, JsonNodePath) 262 * @return the evaluation parent schema 263 */ getEvaluationParentSchema()264 public JsonSchema getEvaluationParentSchema() { 265 if (this.evaluationParentSchema != null) { 266 return this.evaluationParentSchema; 267 } 268 return getParentSchema(); 269 } 270 fetchSubSchemaNode(ValidationContext validationContext)271 protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) { 272 return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext); 273 } 274 validate(ExecutionContext executionContext, JsonNode node)275 public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node) { 276 return validate(executionContext, node, node, atRoot()); 277 } 278 getNodeFieldType()279 protected String getNodeFieldType() { 280 JsonNode typeField = this.getParentSchema().getSchemaNode().get("type"); 281 if (typeField != null) { 282 return typeField.asText(); 283 } 284 return null; 285 } 286 preloadJsonSchemas(final Collection<JsonSchema> schemas)287 protected void preloadJsonSchemas(final Collection<JsonSchema> schemas) { 288 for (final JsonSchema schema : schemas) { 289 schema.initializeValidators(); 290 } 291 } 292 293 public static class JsonNodePathLegacy { 294 private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.LEGACY); getInstance()295 public static JsonNodePath getInstance() { 296 return INSTANCE; 297 } 298 } 299 300 public static class JsonNodePathJsonPointer { 301 private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_POINTER); getInstance()302 public static JsonNodePath getInstance() { 303 return INSTANCE; 304 } 305 } 306 307 public static class JsonNodePathJsonPath { 308 private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_PATH); getInstance()309 public static JsonNodePath getInstance() { 310 return INSTANCE; 311 } 312 } 313 314 /** 315 * Get the root path. 316 * 317 * @return The path. 318 */ atRoot()319 protected JsonNodePath atRoot() { 320 if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_POINTER)) { 321 return JsonNodePathJsonPointer.getInstance(); 322 } else if (this.validationContext.getConfig().getPathType().equals(PathType.LEGACY)) { 323 return JsonNodePathLegacy.getInstance(); 324 } else if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_PATH)) { 325 return JsonNodePathJsonPath.getInstance(); 326 } 327 return new JsonNodePath(this.validationContext.getConfig().getPathType()); 328 } 329 330 @Override toString()331 public String toString() { 332 return getEvaluationPath().getName(-1); 333 } 334 335 /** 336 * Determines if the keyword exists adjacent in the evaluation path. 337 * <p> 338 * This does not check if the keyword exists in the current meta schema as this 339 * can be a cross-draft case where the properties keyword is in a Draft 7 schema 340 * and the unevaluatedProperties keyword is in an outer Draft 2020-12 schema. 341 * <p> 342 * The fact that the validator exists in the evaluation path implies that the 343 * keyword was valid in whatever meta schema for that schema it was created for. 344 * 345 * @param keyword the keyword to check 346 * @return true if found 347 */ hasAdjacentKeywordInEvaluationPath(String keyword)348 protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { 349 boolean hasValidator = false; 350 JsonSchema schema = getEvaluationParentSchema(); 351 while (schema != null) { 352 for (JsonValidator validator : schema.getValidators()) { 353 if (keyword.equals(validator.getKeyword())) { 354 hasValidator = true; 355 break; 356 } 357 } 358 if (hasValidator) { 359 break; 360 } 361 schema = schema.getEvaluationParentSchema(); 362 } 363 return hasValidator; 364 } 365 366 @Override message()367 protected MessageSourceValidationMessage.Builder message() { 368 return super.message().schemaNode(this.schemaNode); 369 } 370 371 /** 372 * Determine if annotations should be reported. 373 * 374 * @param executionContext the execution context 375 * @return true if annotations should be reported 376 */ collectAnnotations(ExecutionContext executionContext)377 protected boolean collectAnnotations(ExecutionContext executionContext) { 378 return collectAnnotations(executionContext, getKeyword()); 379 } 380 381 /** 382 * Determine if annotations should be reported. 383 * 384 * @param executionContext the execution context 385 * @param keyword the keyword 386 * @return true if annotations should be reported 387 */ collectAnnotations(ExecutionContext executionContext, String keyword)388 protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { 389 return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() 390 && executionContext.getExecutionConfig().getAnnotationCollectionFilter().test(keyword); 391 } 392 393 /** 394 * Puts an annotation. 395 * 396 * @param executionContext the execution context 397 * @param customizer to customize the annotation 398 */ putAnnotation(ExecutionContext executionContext, Consumer<JsonNodeAnnotation.Builder> customizer)399 protected void putAnnotation(ExecutionContext executionContext, Consumer<JsonNodeAnnotation.Builder> customizer) { 400 JsonNodeAnnotation.Builder builder = JsonNodeAnnotation.builder().evaluationPath(this.evaluationPath) 401 .schemaLocation(this.schemaLocation).keyword(getKeyword()); 402 customizer.accept(builder); 403 executionContext.getAnnotations().put(builder.build()); 404 } 405 } 406