1 /* 2 * Copyright (c) 2023 the original author or authors. 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 package com.networknt.schema; 17 18 import java.util.Objects; 19 20 /** 21 * The schema location is the canonical IRI of the schema object plus a JSON 22 * Pointer fragment indicating the subschema that produced a result. In contrast 23 * with the evaluation path, the schema location MUST NOT include by-reference 24 * applicators such as $ref or $dynamicRef. 25 */ 26 public class SchemaLocation { 27 private static final JsonNodePath JSON_POINTER = new JsonNodePath(PathType.JSON_POINTER); 28 private static final JsonNodePath ANCHOR = new JsonNodePath(PathType.URI_REFERENCE); 29 30 /** 31 * Represents a relative schema location to the current document. 32 */ 33 public static final SchemaLocation DOCUMENT = new SchemaLocation(null, JSON_POINTER); 34 35 private final AbsoluteIri absoluteIri; 36 private final JsonNodePath fragment; 37 38 private volatile String value = null; // computed lazily 39 40 /** 41 * Constructs a new {@link SchemaLocation}. 42 * 43 * @param absoluteIri canonical absolute IRI of the schema object 44 * @param fragment the fragment 45 */ SchemaLocation(AbsoluteIri absoluteIri, JsonNodePath fragment)46 public SchemaLocation(AbsoluteIri absoluteIri, JsonNodePath fragment) { 47 this.absoluteIri = absoluteIri; 48 this.fragment = fragment; 49 } 50 51 /** 52 * Constructs a new {@link SchemaLocation}. 53 * 54 * @param absoluteIri canonical absolute IRI of the schema object 55 */ SchemaLocation(AbsoluteIri absoluteIri)56 public SchemaLocation(AbsoluteIri absoluteIri) { 57 this(absoluteIri, JSON_POINTER); 58 } 59 60 /** 61 * Gets the canonical absolute IRI of the schema object. 62 * <p> 63 * This is a unique identifier indicated by the $id property or id property in 64 * Draft 4 and earlier. This does not have to be network accessible. 65 * 66 * @return the canonical absolute IRI of the schema object. 67 */ getAbsoluteIri()68 public AbsoluteIri getAbsoluteIri() { 69 return this.absoluteIri; 70 } 71 72 /** 73 * Gets the fragment. 74 * 75 * @return the fragment 76 */ getFragment()77 public JsonNodePath getFragment() { 78 return this.fragment; 79 } 80 81 /** 82 * Appends the token to the fragment. 83 * 84 * @param token the segment 85 * @return a new schema location with the segment 86 */ append(String token)87 public SchemaLocation append(String token) { 88 return new SchemaLocation(this.absoluteIri, this.fragment.append(token)); 89 } 90 91 /** 92 * Appends the index to the fragment. 93 * 94 * @param index the segment 95 * @return a new schema location with the segment 96 */ append(int index)97 public SchemaLocation append(int index) { 98 return new SchemaLocation(this.absoluteIri, this.fragment.append(index)); 99 } 100 101 /** 102 * Parses a string representing an IRI of the schema location. 103 * 104 * @param iri the IRI 105 * @return the schema location 106 */ of(String iri)107 public static SchemaLocation of(String iri) { 108 if (iri == null) { 109 return null; 110 } 111 if ("#".equals(iri)) { 112 return DOCUMENT; 113 } 114 String[] iriParts = iri.split("#"); 115 AbsoluteIri absoluteIri = null; 116 JsonNodePath fragment = JSON_POINTER; 117 if (iriParts.length > 0) { 118 absoluteIri = AbsoluteIri.of(iriParts[0]); 119 } 120 if (iriParts.length > 1) { 121 fragment = Fragment.of(iriParts[1]); 122 } 123 return new SchemaLocation(absoluteIri, fragment); 124 } 125 126 /** 127 * Resolves against a absolute IRI reference or fragment. 128 * 129 * @param absoluteIriReferenceOrFragment to resolve 130 * @return the resolved schema location 131 */ resolve(String absoluteIriReferenceOrFragment)132 public SchemaLocation resolve(String absoluteIriReferenceOrFragment) { 133 if (absoluteIriReferenceOrFragment == null) { 134 return this; 135 } 136 if ("#".equals(absoluteIriReferenceOrFragment)) { 137 return new SchemaLocation(this.getAbsoluteIri(), JSON_POINTER); 138 } 139 JsonNodePath fragment = JSON_POINTER; 140 String[] parts = absoluteIriReferenceOrFragment.split("#"); 141 AbsoluteIri absoluteIri = this.getAbsoluteIri(); 142 if (absoluteIri != null) { 143 if (!parts[0].isEmpty()) { 144 absoluteIri = absoluteIri.resolve(parts[0]); 145 } 146 } else { 147 absoluteIri = AbsoluteIri.of(parts[0]); 148 } 149 if (parts.length > 1 && !parts[1].isEmpty()) { 150 fragment = Fragment.of(parts[1]); 151 } 152 return new SchemaLocation(absoluteIri, fragment); 153 } 154 155 /** 156 * Resolves against a absolute IRI reference or fragment. 157 * 158 * @param schemaLocation the parent 159 * @param absoluteIriReferenceOrFragment to resolve 160 * @return the resolved schema location 161 */ resolve(SchemaLocation schemaLocation, String absoluteIriReferenceOrFragment)162 public static String resolve(SchemaLocation schemaLocation, String absoluteIriReferenceOrFragment) { 163 if ("#".equals(absoluteIriReferenceOrFragment)) { 164 return schemaLocation.getAbsoluteIri().toString() + "#"; 165 } 166 String[] parts = absoluteIriReferenceOrFragment.split("#"); 167 AbsoluteIri absoluteIri = schemaLocation.getAbsoluteIri(); 168 String resolved = parts[0]; 169 if (absoluteIri != null) { 170 if (!parts[0].isEmpty()) { 171 resolved = absoluteIri.resolve(parts[0]).toString(); 172 } else { 173 resolved = absoluteIri.toString(); 174 } 175 } 176 if (parts.length > 1 && !parts[1].isEmpty()) { 177 resolved = resolved + "#" + parts[1]; 178 } else { 179 resolved = resolved + "#"; 180 } 181 return resolved; 182 } 183 184 /** 185 * The fragment can be a JSON pointer to the document or an anchor. 186 */ 187 public static class Fragment { 188 /** 189 * Parses a string representing a fragment. 190 * 191 * @param fragmentString the fragment 192 * @return the path 193 */ of(String fragmentString)194 public static JsonNodePath of(String fragmentString) { 195 if (fragmentString.startsWith("#")) { 196 fragmentString = fragmentString.substring(1); 197 } 198 JsonNodePath fragment = JSON_POINTER; 199 String[] fragmentParts = fragmentString.split("/"); 200 201 boolean jsonPointer = false; 202 if (fragmentString.startsWith("/")) { 203 // json pointer 204 jsonPointer = true; 205 } else { 206 // anchor 207 fragment = ANCHOR; 208 } 209 210 int index = -1; 211 for (int fragmentPartIndex = 0; fragmentPartIndex < fragmentParts.length; fragmentPartIndex++) { 212 if (fragmentPartIndex == 0 && jsonPointer) { 213 continue; 214 } 215 String fragmentPart = fragmentParts[fragmentPartIndex]; 216 for (int x = 0; x < fragmentPart.length(); x++) { 217 char ch = fragmentPart.charAt(x); 218 if (ch >= '0' && ch <= '9') { 219 if (x == 0) { 220 index = 0; 221 } else { 222 index = index * 10; 223 } 224 index += (ch - '0'); 225 } else { 226 index = -1; // Not an index 227 break; 228 } 229 } 230 if (index != -1) { 231 fragment = fragment.append(index); 232 } else { 233 fragment = fragment.append(fragmentPart.toString()); 234 } 235 } 236 if (index == -1 && fragmentString.endsWith("/")) { 237 // Trailing / in fragment 238 fragment = fragment.append(""); 239 } 240 return fragment; 241 } 242 243 /** 244 * Determine if the string is a fragment. 245 * 246 * @param fragmentString to evaluate 247 * @return true if it is a fragment 248 */ isFragment(String fragmentString)249 public static boolean isFragment(String fragmentString) { 250 return fragmentString.startsWith("#"); 251 } 252 253 /** 254 * Determine if the string is a JSON Pointer fragment. 255 * 256 * @param fragmentString to evaluate 257 * @return true if it is a JSON Pointer fragment 258 */ isJsonPointerFragment(String fragmentString)259 public static boolean isJsonPointerFragment(String fragmentString) { 260 return fragmentString.startsWith("#/"); 261 } 262 263 /** 264 * Determine if the string is an anchor fragment. 265 * 266 * @param fragmentString to evaluate 267 * @return true if it is an anchor fragment 268 */ isAnchorFragment(String fragmentString)269 public static boolean isAnchorFragment(String fragmentString) { 270 return isFragment(fragmentString) && !isDocumentFragment(fragmentString) 271 && !isJsonPointerFragment(fragmentString); 272 } 273 274 /** 275 * Determine if the string is a fragment referencing the document. 276 * 277 * @param fragmentString to evaluate 278 * @return true if it is a fragment 279 */ isDocumentFragment(String fragmentString)280 public static boolean isDocumentFragment(String fragmentString) { 281 return "#".equals(fragmentString); 282 } 283 } 284 285 /** 286 * Returns a builder for building {@link SchemaLocation}. 287 * 288 * @return the builder 289 */ builder()290 public static Builder builder() { 291 return new Builder(); 292 } 293 294 /** 295 * Builder for building {@link SchemaLocation}. 296 */ 297 public static class Builder { 298 private AbsoluteIri absoluteIri; 299 private JsonNodePath fragment = JSON_POINTER; 300 301 /** 302 * Sets the canonical absolute IRI of the schema object. 303 * <p> 304 * This is a unique identifier indicated by the $id property or id property in 305 * Draft 4 and earlier. This does not have to be network accessible. 306 * 307 * @param absoluteIri the canonical IRI of the schema object 308 * @return the builder 309 */ absoluteIri(AbsoluteIri absoluteIri)310 protected Builder absoluteIri(AbsoluteIri absoluteIri) { 311 this.absoluteIri = absoluteIri; 312 return this; 313 } 314 315 /** 316 * Sets the canonical absolute IRI of the schema object. 317 * <p> 318 * This is a unique identifier indicated by the $id property or id property in 319 * Draft 4 and earlier. This does not have to be network accessible. 320 * 321 * @param absoluteIri the canonical IRI of the schema object 322 * @return the builder 323 */ absoluteIri(String absoluteIri)324 protected Builder absoluteIri(String absoluteIri) { 325 return absoluteIri(AbsoluteIri.of(absoluteIri)); 326 } 327 328 /** 329 * Sets the fragment. 330 * 331 * @param fragment the fragment 332 * @return the builder 333 */ fragment(JsonNodePath fragment)334 protected Builder fragment(JsonNodePath fragment) { 335 this.fragment = fragment; 336 return this; 337 } 338 339 /** 340 * Sets the fragment. 341 * 342 * @param fragment the fragment 343 * @return the builder 344 */ fragment(String fragment)345 protected Builder fragment(String fragment) { 346 return fragment(Fragment.of(fragment)); 347 } 348 349 /** 350 * Builds a {@link SchemaLocation}. 351 * 352 * @return the schema location 353 */ build()354 public SchemaLocation build() { 355 return new SchemaLocation(absoluteIri, fragment); 356 } 357 358 } 359 360 @Override toString()361 public String toString() { 362 if (this.value == null) { 363 if (this.absoluteIri != null && this.fragment == null) { 364 this.value = this.absoluteIri.toString(); 365 } else { 366 StringBuilder result = new StringBuilder(); 367 if (this.absoluteIri != null) { 368 result.append(this.absoluteIri.toString()); 369 } 370 result.append("#"); 371 if (this.fragment != null) { 372 result.append(this.fragment.toString()); 373 } 374 this.value = result.toString(); 375 } 376 } 377 return this.value; 378 } 379 380 @Override hashCode()381 public int hashCode() { 382 return Objects.hash(fragment, absoluteIri); 383 } 384 385 @Override equals(Object obj)386 public boolean equals(Object obj) { 387 if (this == obj) 388 return true; 389 if (obj == null) 390 return false; 391 if (getClass() != obj.getClass()) 392 return false; 393 SchemaLocation other = (SchemaLocation) obj; 394 return Objects.equals(fragment, other.fragment) && Objects.equals(absoluteIri, other.absoluteIri); 395 } 396 } 397