• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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