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