• 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.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