• 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 org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 
23 import java.util.*;
24 
25 /**
26  * {@link JsonValidator} that resolves $ref.
27  */
28 public class RefValidator extends BaseJsonValidator {
29     private static final Logger logger = LoggerFactory.getLogger(RefValidator.class);
30 
31     protected final JsonSchemaRef schema;
32 
33     private static final String REF_CURRENT = "#";
34 
RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)35     public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
36         super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext);
37         String refValue = schemaNode.asText();
38         this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath);
39     }
40 
getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, JsonNodePath evaluationPath)41     static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue,
42             JsonNodePath evaluationPath) {
43         // The evaluationPath is used to derive the keywordLocation
44         final String refValueOriginal = refValue;
45 
46         if (!refValue.startsWith(REF_CURRENT)) {
47             // This will be the uri extracted from the refValue (this may be a relative or absolute uri).
48             final String refUri;
49             final int index = refValue.indexOf(REF_CURRENT);
50             if (index > 0) {
51                 refUri = refValue.substring(0, index);
52             } else {
53                 refUri = refValue;
54             }
55 
56             // This will determine the correct absolute uri for the refUri. This decision will take into
57             // account the current uri of the parent schema.
58             String schemaUriFinal = resolve(parentSchema, refUri);
59             SchemaLocation schemaLocation = SchemaLocation.of(schemaUriFinal);
60             // This should retrieve schemas regardless of the protocol that is in the uri.
61             return new JsonSchemaRef(new CachedSupplier<>(() -> {
62                 JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal);
63                 if (schemaResource == null) {
64                     schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig());
65                     if (schemaResource != null) {
66                         copySchemaResources(validationContext, schemaResource);
67                     }
68                 }
69                 if (index < 0) {
70                     if (schemaResource == null) {
71                         return null;
72                     }
73                     return schemaResource.fromRef(parentSchema, evaluationPath);
74                 } else {
75                     String newRefValue = refValue.substring(index);
76                     String find = schemaLocation.getAbsoluteIri() + newRefValue;
77                     JsonSchema findSchemaResource = validationContext.getSchemaResources().get(find);
78                     if (findSchemaResource == null) {
79                         findSchemaResource = validationContext.getDynamicAnchors().get(find);
80                     }
81                     if (findSchemaResource != null) {
82                         schemaResource = findSchemaResource;
83                     } else {
84                         schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal,
85                                 evaluationPath);
86                     }
87                     if (schemaResource == null) {
88                         return null;
89                     }
90                     return schemaResource.fromRef(parentSchema, evaluationPath);
91                 }
92             }));
93 
94         } else if (SchemaLocation.Fragment.isAnchorFragment(refValue)) {
95             String absoluteIri = resolve(parentSchema, refValue);
96             // Schema resource needs to update the parent and evaluation path
97             return new JsonSchemaRef(new CachedSupplier<>(() -> {
98                 JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri);
99                 if (schemaResource == null) {
100                     schemaResource = validationContext.getDynamicAnchors().get(absoluteIri);
101                 }
102                 if (schemaResource == null) {
103                     schemaResource = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath);
104                 }
105                 if (schemaResource == null) {
106                     return null;
107                 }
108                 return schemaResource.fromRef(parentSchema, evaluationPath);
109             }));
110         }
111         if (refValue.equals(REF_CURRENT)) {
112             return new JsonSchemaRef(new CachedSupplier<>(
113                     () -> parentSchema.findSchemaResourceRoot().fromRef(parentSchema, evaluationPath)));
114         }
115         return new JsonSchemaRef(new CachedSupplier<>(
116                 () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath)
117                         .fromRef(parentSchema, evaluationPath)));
118     }
119 
120     private static void copySchemaResources(ValidationContext validationContext, JsonSchema schemaResource) {
121         if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) {
122             validationContext.getSchemaResources()
123                     .putAll(schemaResource.getValidationContext().getSchemaResources());
124         }
125         if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) {
126             validationContext.getSchemaReferences()
127                     .putAll(schemaResource.getValidationContext().getSchemaReferences());
128         }
129         if (!schemaResource.getValidationContext().getDynamicAnchors().isEmpty()) {
130             validationContext.getDynamicAnchors()
131                     .putAll(schemaResource.getValidationContext().getDynamicAnchors());
132         }
133     }
134 
135     private static String resolve(JsonSchema parentSchema, String refValue) {
136         // $ref prevents a sibling $id from changing the base uri
137         JsonSchema base = parentSchema;
138         if (parentSchema.getId() != null && parentSchema.parentSchema != null) {
139             base = parentSchema.parentSchema;
140         }
141         return SchemaLocation.resolve(base.getSchemaLocation(), refValue);
142     }
143 
144     private static JsonSchema getJsonSchema(JsonSchema parent,
145                                                   ValidationContext validationContext,
146                                                   String refValue,
147                                                   String refValueOriginal,
148                                                   JsonNodePath evaluationPath) {
149         // This should be processing json pointer fragments only
150         JsonNodePath fragment = SchemaLocation.Fragment.of(refValue);
151         String schemaReference = resolve(parent, refValueOriginal);
152         // ConcurrentHashMap computeIfAbsent does not allow calls that result in a
153         // recursive update to the map.
154         // The getSubSchema potentially recurses to call back to getJsonSchema again
155         JsonSchema result = validationContext.getSchemaReferences().get(schemaReference);
156         if (result == null) {
157             synchronized (validationContext.getJsonSchemaFactory()) { // acquire lock on shared factory object to prevent deadlock
158                 result = validationContext.getSchemaReferences().get(schemaReference);
159                 if (result == null) {
160                     result = parent.getSubSchema(fragment);
161                     if (result != null) {
162                         validationContext.getSchemaReferences().put(schemaReference, result);
163                     }
164                 }
165             }
166         }
167         return result;
168     }
169 
170     @Override
171     public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
172         debug(logger, node, rootNode, instanceLocation);
173         JsonSchema refSchema = this.schema.getSchema();
174         if (refSchema == null) {
175             ValidationMessage validationMessage = message().type(ValidatorTypeCode.REF.getValue())
176                     .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
177                     .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
178                     .arguments(schemaNode.asText()).build();
179             throw new InvalidSchemaRefException(validationMessage);
180         }
181         return refSchema.validate(executionContext, node, rootNode, instanceLocation);
182     }
183 
184     @Override
185     public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
186         debug(logger, node, rootNode, instanceLocation);
187         // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
188         // these schemas will be cached along with config. We have to replace the config for cached $ref references
189         // with the latest config. Reset the config.
190         JsonSchema refSchema = this.schema.getSchema();
191         if (refSchema == null) {
192             ValidationMessage validationMessage = message().type(ValidatorTypeCode.REF.getValue())
193                     .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
194                     .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
195                     .arguments(schemaNode.asText()).build();
196             throw new InvalidSchemaRefException(validationMessage);
197         }
198         return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
199     }
200 
201 	public JsonSchemaRef getSchemaRef() {
202 		return this.schema;
203 	}
204 
205     @Override
206     public void preloadJsonSchema() {
207         JsonSchema jsonSchema = null;
208         try {
209             jsonSchema = this.schema.getSchema();
210         } catch (JsonSchemaException e) {
211             throw e;
212         } catch (RuntimeException e) {
213             throw new JsonSchemaException(e);
214         }
215         // Check for circular dependency
216         // Only one cycle is pre-loaded
217         // The rest of the cycles will load at execution time depending on the input
218         // data
219         SchemaLocation schemaLocation = jsonSchema.getSchemaLocation();
220         JsonSchema check = jsonSchema;
221         boolean circularDependency = false;
222         while (check.getEvaluationParentSchema() != null) {
223             check = check.getEvaluationParentSchema();
224             if (check.getSchemaLocation().equals(schemaLocation)) {
225                 circularDependency = true;
226                 break;
227             }
228         }
229         if (!circularDependency) {
230             jsonSchema.initializeValidators();
231         }
232     }
233 }
234