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 * Represents a path to a JSON node. 22 */ 23 public class JsonNodePath implements Comparable<JsonNodePath> { 24 private final PathType type; 25 private final JsonNodePath parent; 26 27 private final String pathSegment; 28 private final int pathSegmentIndex; 29 30 private volatile String value = null; // computed lazily 31 JsonNodePath(PathType type)32 public JsonNodePath(PathType type) { 33 this.type = type; 34 this.parent = null; 35 this.pathSegment = null; 36 this.pathSegmentIndex = -1; 37 } 38 JsonNodePath(JsonNodePath parent, String pathSegment)39 private JsonNodePath(JsonNodePath parent, String pathSegment) { 40 this.parent = parent; 41 this.type = parent.type; 42 this.pathSegment = pathSegment; 43 this.pathSegmentIndex = -1; 44 } 45 JsonNodePath(JsonNodePath parent, int pathSegmentIndex)46 private JsonNodePath(JsonNodePath parent, int pathSegmentIndex) { 47 this.parent = parent; 48 this.type = parent.type; 49 this.pathSegment = null; 50 this.pathSegmentIndex = pathSegmentIndex; 51 } 52 53 /** 54 * Returns the parent path, or null if this path does not have a parent. 55 * 56 * @return the parent 57 */ getParent()58 public JsonNodePath getParent() { 59 return this.parent; 60 } 61 62 /** 63 * Append the child token to the path. 64 * 65 * @param token the child token 66 * @return the path 67 */ append(String token)68 public JsonNodePath append(String token) { 69 return new JsonNodePath(this, token); 70 } 71 72 /** 73 * Append the index to the path. 74 * 75 * @param index the index 76 * @return the path 77 */ append(int index)78 public JsonNodePath append(int index) { 79 return new JsonNodePath(this, index); 80 } 81 82 /** 83 * Gets the {@link PathType}. 84 * 85 * @return the path type 86 */ getPathType()87 public PathType getPathType() { 88 return this.type; 89 } 90 91 /** 92 * Gets the name element given an index. 93 * <p> 94 * The index parameter is the index of the name element to return. The element 95 * that is closest to the root has index 0. The element that is farthest from 96 * the root has index count -1. 97 * 98 * @param index to return 99 * @return the name element 100 */ getName(int index)101 public String getName(int index) { 102 Object element = getElement(index); 103 if (element != null) { 104 return element.toString(); 105 } 106 return null; 107 } 108 109 /** 110 * Gets the element given an index. 111 * <p> 112 * The index parameter is the index of the element to return. The element that 113 * is closest to the root has index 0. The element that is farthest from the 114 * root has index count -1. 115 * 116 * @param index to return 117 * @return the element either a String or Integer 118 */ getElement(int index)119 public Object getElement(int index) { 120 if (index == -1) { 121 if (this.pathSegmentIndex != -1) { 122 return Integer.valueOf(this.pathSegmentIndex); 123 } else { 124 return this.pathSegment; 125 } 126 } 127 int nameCount = getNameCount(); 128 if (nameCount - 1 == index) { 129 return this.getElement(-1); 130 } 131 int count = nameCount - index - 1; 132 if (count < 0) { 133 throw new IllegalArgumentException(""); 134 } 135 JsonNodePath current = this; 136 for (int x = 0; x < count; x++) { 137 current = current.parent; 138 } 139 return current.getElement(-1); 140 } 141 142 /** 143 * Gets the number of name elements in the path. 144 * 145 * @return the number of elements in the path or 0 if this is the root element 146 */ getNameCount()147 public int getNameCount() { 148 int current = this.pathSegmentIndex == -1 && this.pathSegment == null ? 0 : 1; 149 int parent = this.parent != null ? this.parent.getNameCount() : 0; 150 return current + parent; 151 } 152 153 /** 154 * Tests if this path starts with the other path. 155 * 156 * @param other the other path 157 * @return true if the path starts with the other path 158 */ startsWith(JsonNodePath other)159 public boolean startsWith(JsonNodePath other) { 160 int count = getNameCount(); 161 int otherCount = other.getNameCount(); 162 163 if (otherCount > count) { 164 return false; 165 } else if (otherCount == count) { 166 return this.equals(other); 167 } else { 168 JsonNodePath compare = this; 169 int x = count - otherCount; 170 while (x > 0) { 171 compare = compare.getParent(); 172 x--; 173 } 174 return other.equals(compare); 175 } 176 } 177 178 /** 179 * Tests if this path contains a string segment that is an exact match. 180 * <p> 181 * This will not match if the segment is a number. 182 * 183 * @param segment the segment to test 184 * @return true if the string segment is found 185 */ contains(String segment)186 public boolean contains(String segment) { 187 boolean result = segment.equals(this.pathSegment); 188 if (result) { 189 return true; 190 } 191 JsonNodePath path = this.getParent(); 192 while (path != null) { 193 if (segment.equals(path.pathSegment)) { 194 return true; 195 } 196 path = path.getParent(); 197 } 198 return false; 199 } 200 201 @Override toString()202 public String toString() { 203 if (this.value == null) { 204 String parentValue = this.parent == null ? type.getRoot() : this.parent.toString(); 205 if (pathSegmentIndex != -1) { 206 this.value = this.type.append(parentValue, pathSegmentIndex); 207 } else if (pathSegment != null) { 208 this.value = this.type.append(parentValue, pathSegment); 209 } else { 210 this.value = parentValue; 211 } 212 } 213 return this.value; 214 } 215 216 @Override hashCode()217 public int hashCode() { 218 return Objects.hash(parent, pathSegment, pathSegmentIndex, type); 219 } 220 221 @Override equals(Object obj)222 public boolean equals(Object obj) { 223 if (this == obj) 224 return true; 225 if (obj == null) 226 return false; 227 if (getClass() != obj.getClass()) 228 return false; 229 JsonNodePath other = (JsonNodePath) obj; 230 return Objects.equals(parent, other.parent) && Objects.equals(pathSegment, other.pathSegment) 231 && pathSegmentIndex == other.pathSegmentIndex && type == other.type; 232 } 233 234 @Override compareTo(JsonNodePath other)235 public int compareTo(JsonNodePath other) { 236 if (this.parent != null && other.parent == null) { 237 return 1; 238 } else if (this.parent == null && other.parent != null) { 239 return -1; 240 } else if (this.parent != null && other.parent != null) { 241 int result = this.parent.compareTo(other.parent); 242 if (result != 0) { 243 return result; 244 } 245 } 246 String thisValue = this.getName(-1); 247 String otherValue = other.getName(-1); 248 if (thisValue == null && otherValue == null) { 249 return 0; 250 } else if (thisValue != null && otherValue == null) { 251 return 1; 252 } else if (thisValue == null && otherValue != null) { 253 return -1; 254 } else { 255 return thisValue.compareTo(otherValue); 256 } 257 } 258 } 259