1 package com.fasterxml.jackson.core; 2 3 import com.fasterxml.jackson.core.io.NumberInput; 4 5 /** 6 * Implementation of 7 * <a href="http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-03">JSON Pointer</a> 8 * specification. 9 * Pointer instances can be used to locate logical JSON nodes for things like 10 * tree traversal (see {@link TreeNode#at}). 11 * It may be used in future for filtering of streaming JSON content 12 * as well (not implemented yet for 2.3). 13 *<p> 14 * Instances are fully immutable and can be cached, shared between threads. 15 * 16 * @author Tatu Saloranta 17 * 18 * @since 2.3 19 */ 20 public class JsonPointer 21 { 22 /** 23 * Character used to separate segments. 24 * 25 * @since 2.9 26 */ 27 public final static char SEPARATOR = '/'; 28 29 /** 30 * Marker instance used to represent segment that matches current 31 * node or position (that is, returns true for 32 * {@link #matches()}). 33 */ 34 protected final static JsonPointer EMPTY = new JsonPointer(); 35 36 /** 37 * Reference to rest of the pointer beyond currently matching 38 * segment (if any); null if this pointer refers to the matching 39 * segment. 40 */ 41 protected final JsonPointer _nextSegment; 42 43 /** 44 * Reference from currently matching segment (if any) to node 45 * before leaf. 46 * Lazily constructed if/as needed. 47 *<p> 48 * NOTE: we'll use `volatile` here assuming that this is unlikely to 49 * become a performance bottleneck. If it becomes one we can probably 50 * just drop it and things still should work (despite warnings as per JMM 51 * regarding visibility (and lack thereof) of unguarded changes). 52 * 53 * @since 2.5 54 */ 55 protected volatile JsonPointer _head; 56 57 /** 58 * We will retain representation of the pointer, as a String, 59 * so that {@link #toString} should be as efficient as possible. 60 */ 61 protected final String _asString; 62 63 protected final String _matchingPropertyName; 64 65 protected final int _matchingElementIndex; 66 67 /* 68 /********************************************************** 69 /* Construction 70 /********************************************************** 71 */ 72 73 /** 74 * Constructor used for creating "empty" instance, used to represent 75 * state that matches current node. 76 */ JsonPointer()77 protected JsonPointer() { 78 _nextSegment = null; 79 _matchingPropertyName = ""; 80 _matchingElementIndex = -1; 81 _asString = ""; 82 } 83 84 /** 85 * Constructor used for creating non-empty Segments 86 */ JsonPointer(String fullString, String segment, JsonPointer next)87 protected JsonPointer(String fullString, String segment, JsonPointer next) { 88 _asString = fullString; 89 _nextSegment = next; 90 // Ok; may always be a property 91 _matchingPropertyName = segment; 92 // but could be an index, if parsable 93 _matchingElementIndex = _parseIndex(segment); 94 } 95 96 /** 97 * @since 2.5 98 */ JsonPointer(String fullString, String segment, int matchIndex, JsonPointer next)99 protected JsonPointer(String fullString, String segment, int matchIndex, JsonPointer next) { 100 _asString = fullString; 101 _nextSegment = next; 102 _matchingPropertyName = segment; 103 _matchingElementIndex = matchIndex; 104 } 105 106 /* 107 /********************************************************** 108 /* Factory methods 109 /********************************************************** 110 */ 111 112 /** 113 * Factory method that parses given input and construct matching pointer 114 * instance, if it represents a valid JSON Pointer: if not, a 115 * {@link IllegalArgumentException} is thrown. 116 * 117 * @throws IllegalArgumentException Thrown if the input does not present a valid JSON Pointer 118 * expression: currently the only such expression is one that does NOT start with 119 * a slash ('/'). 120 */ compile(String input)121 public static JsonPointer compile(String input) throws IllegalArgumentException 122 { 123 // First quick checks for well-known 'empty' pointer 124 if ((input == null) || input.length() == 0) { 125 return EMPTY; 126 } 127 // And then quick validity check: 128 if (input.charAt(0) != '/') { 129 throw new IllegalArgumentException("Invalid input: JSON Pointer expression must start with '/': "+"\""+input+"\""); 130 } 131 return _parseTail(input); 132 } 133 134 /** 135 * Alias for {@link #compile}; added to make instances automatically 136 * deserializable by Jackson databind. 137 */ valueOf(String input)138 public static JsonPointer valueOf(String input) { return compile(input); } 139 140 /** 141 * Accessor for an "empty" expression, that is, one you can get by 142 * calling {@link #compile} with "" (empty String). 143 *<p> 144 * NOTE: this is different from expression for {@code "/"} which would 145 * instead match Object node property with empty String ("") as name. 146 * 147 * @since 2.10 148 */ empty()149 public static JsonPointer empty() { return EMPTY; } 150 151 /** 152 * Factory method that will construct a pointer instance that describes 153 * path to location given {@link JsonStreamContext} points to. 154 * 155 * @param context Context to build pointer expression fot 156 * @param includeRoot Whether to include number offset for virtual "root context" 157 * or not. 158 * 159 * @since 2.9 160 */ forPath(JsonStreamContext context, boolean includeRoot)161 public static JsonPointer forPath(JsonStreamContext context, 162 boolean includeRoot) 163 { 164 // First things first: last segment may be for START_ARRAY/START_OBJECT, 165 // in which case it does not yet point to anything, and should be skipped 166 if (context == null) { 167 return EMPTY; 168 } 169 if (!context.hasPathSegment()) { 170 // one special case; do not prune root if we need it 171 if (!(includeRoot && context.inRoot() && context.hasCurrentIndex())) { 172 context = context.getParent(); 173 } 174 } 175 JsonPointer tail = null; 176 177 for (; context != null; context = context.getParent()) { 178 if (context.inObject()) { 179 String seg = context.getCurrentName(); 180 if (seg == null) { // is this legal? 181 seg = ""; 182 } 183 tail = new JsonPointer(_fullPath(tail, seg), seg, tail); 184 } else if (context.inArray() || includeRoot) { 185 int ix = context.getCurrentIndex(); 186 String ixStr = String.valueOf(ix); 187 tail = new JsonPointer(_fullPath(tail, ixStr), ixStr, ix, tail); 188 } 189 // NOTE: this effectively drops ROOT node(s); should have 1 such node, 190 // as the last one, but we don't have to care (probably some paths have 191 // no root, for example) 192 } 193 if (tail == null) { 194 return EMPTY; 195 } 196 return tail; 197 } 198 _fullPath(JsonPointer tail, String segment)199 private static String _fullPath(JsonPointer tail, String segment) 200 { 201 if (tail == null) { 202 StringBuilder sb = new StringBuilder(segment.length()+1); 203 sb.append('/'); 204 _appendEscaped(sb, segment); 205 return sb.toString(); 206 } 207 String tailDesc = tail._asString; 208 StringBuilder sb = new StringBuilder(segment.length() + 1 + tailDesc.length()); 209 sb.append('/'); 210 _appendEscaped(sb, segment); 211 sb.append(tailDesc); 212 return sb.toString(); 213 } 214 _appendEscaped(StringBuilder sb, String segment)215 private static void _appendEscaped(StringBuilder sb, String segment) 216 { 217 for (int i = 0, end = segment.length(); i < end; ++i) { 218 char c = segment.charAt(i); 219 if (c == '/') { 220 sb.append("~1"); 221 continue; 222 } 223 if (c == '~') { 224 sb.append("~0"); 225 continue; 226 } 227 sb.append(c); 228 } 229 } 230 231 /* Factory method that composes a pointer instance, given a set 232 * of 'raw' segments: raw meaning that no processing will be done, 233 * no escaping may is present. 234 * 235 * @param segments 236 * 237 * @return Constructed path instance 238 */ 239 /* TODO! 240 public static JsonPointer fromSegment(String... segments) 241 { 242 if (segments.length == 0) { 243 return EMPTY; 244 } 245 JsonPointer prev = null; 246 247 for (String segment : segments) { 248 JsonPointer next = new JsonPointer() 249 } 250 } 251 */ 252 253 /* 254 /********************************************************** 255 /* Public API 256 /********************************************************** 257 */ 258 matches()259 public boolean matches() { return _nextSegment == null; } getMatchingProperty()260 public String getMatchingProperty() { return _matchingPropertyName; } getMatchingIndex()261 public int getMatchingIndex() { return _matchingElementIndex; } 262 263 /** 264 * @return True if the root selector matches property name (that is, could 265 * match field value of JSON Object node) 266 */ mayMatchProperty()267 public boolean mayMatchProperty() { return _matchingPropertyName != null; } 268 269 /** 270 * @return True if the root selector matches element index (that is, could 271 * match an element of JSON Array node) 272 */ mayMatchElement()273 public boolean mayMatchElement() { return _matchingElementIndex >= 0; } 274 275 /** 276 * Returns the leaf of current JSON Pointer expression. 277 * Leaf is the last non-null segment of current JSON Pointer. 278 * 279 * @since 2.5 280 */ last()281 public JsonPointer last() { 282 JsonPointer current = this; 283 if (current == EMPTY) { 284 return null; 285 } 286 JsonPointer next; 287 while ((next = current._nextSegment) != JsonPointer.EMPTY) { 288 current = next; 289 } 290 return current; 291 } 292 293 /** 294 * Mutant factory method that will return 295 *<ul> 296 * <li>`tail` if `this` instance is "empty" pointer, OR 297 * </li> 298 * <li>`this` instance if `tail` is "empty" pointer, OR 299 * </li> 300 * <li>Newly constructed {@link JsonPointer} instance that starts with all segments 301 * of `this`, followed by all segments of `tail`. 302 * </li> 303 *</ul> 304 * 305 * @param tail {@link JsonPointer} instance to append to this one, to create a new pointer instance 306 * 307 * @return Either `this` instance, `tail`, or a newly created combination, as per description above. 308 */ append(JsonPointer tail)309 public JsonPointer append(JsonPointer tail) { 310 if (this == EMPTY) { 311 return tail; 312 } 313 if (tail == EMPTY) { 314 return this; 315 } 316 // 21-Mar-2017, tatu: Not superbly efficient; could probably improve by not concatenating, 317 // re-decoding -- by stitching together segments -- but for now should be fine. 318 319 String currentJsonPointer = _asString; 320 if (currentJsonPointer.endsWith("/")) { 321 //removes final slash 322 currentJsonPointer = currentJsonPointer.substring(0, currentJsonPointer.length()-1); 323 } 324 return compile(currentJsonPointer + tail._asString); 325 } 326 327 /** 328 * Method that may be called to see if the pointer would match property 329 * (of a JSON Object) with given name. 330 * 331 * @since 2.5 332 */ matchesProperty(String name)333 public boolean matchesProperty(String name) { 334 return (_nextSegment != null) && _matchingPropertyName.equals(name); 335 } 336 matchProperty(String name)337 public JsonPointer matchProperty(String name) { 338 if ((_nextSegment != null) && _matchingPropertyName.equals(name)) { 339 return _nextSegment; 340 } 341 return null; 342 } 343 344 /** 345 * Method that may be called to see if the pointer would match 346 * array element (of a JSON Array) with given index. 347 * 348 * @since 2.5 349 */ matchesElement(int index)350 public boolean matchesElement(int index) { 351 return (index == _matchingElementIndex) && (index >= 0); 352 } 353 354 /** 355 * @since 2.6 356 */ matchElement(int index)357 public JsonPointer matchElement(int index) { 358 if ((index != _matchingElementIndex) || (index < 0)) { 359 return null; 360 } 361 return _nextSegment; 362 } 363 364 /** 365 * Accessor for getting a "sub-pointer", instance where current segment 366 * has been removed and pointer includes rest of segments. 367 * For matching state, will return null. 368 */ tail()369 public JsonPointer tail() { 370 return _nextSegment; 371 } 372 373 /** 374 * Accessor for getting a pointer instance that is identical to this 375 * instance except that the last segment has been dropped. 376 * For example, for JSON Point "/root/branch/leaf", this method would 377 * return pointer "/root/branch" (compared to {@link #tail()} that 378 * would return "/branch/leaf"). 379 * For leaf 380 * 381 * @since 2.5 382 */ head()383 public JsonPointer head() { 384 JsonPointer h = _head; 385 if (h == null) { 386 if (this != EMPTY) { 387 h = _constructHead(); 388 } 389 _head = h; 390 } 391 return h; 392 } 393 394 /* 395 /********************************************************** 396 /* Standard method overrides 397 /********************************************************** 398 */ 399 toString()400 @Override public String toString() { return _asString; } hashCode()401 @Override public int hashCode() { return _asString.hashCode(); } 402 equals(Object o)403 @Override public boolean equals(Object o) { 404 if (o == this) return true; 405 if (o == null) return false; 406 if (!(o instanceof JsonPointer)) return false; 407 return _asString.equals(((JsonPointer) o)._asString); 408 } 409 410 /* 411 /********************************************************** 412 /* Internal methods 413 /********************************************************** 414 */ 415 _parseIndex(String str)416 private final static int _parseIndex(String str) { 417 final int len = str.length(); 418 // [core#133]: beware of super long indexes; assume we never 419 // have arrays over 2 billion entries so ints are fine. 420 if (len == 0 || len > 10) { 421 return -1; 422 } 423 // [core#176]: no leading zeroes allowed 424 char c = str.charAt(0); 425 if (c <= '0') { 426 return (len == 1 && c == '0') ? 0 : -1; 427 } 428 if (c > '9') { 429 return -1; 430 } 431 for (int i = 1; i < len; ++i) { 432 c = str.charAt(i); 433 if (c > '9' || c < '0') { 434 return -1; 435 } 436 } 437 if (len == 10) { 438 long l = NumberInput.parseLong(str); 439 if (l > Integer.MAX_VALUE) { 440 return -1; 441 } 442 } 443 return NumberInput.parseInt(str); 444 } 445 _parseTail(String input)446 protected static JsonPointer _parseTail(String input) { 447 final int end = input.length(); 448 449 // first char is the contextual slash, skip 450 for (int i = 1; i < end; ) { 451 char c = input.charAt(i); 452 if (c == '/') { // common case, got a segment 453 return new JsonPointer(input, input.substring(1, i), 454 _parseTail(input.substring(i))); 455 } 456 ++i; 457 // quoting is different; offline this case 458 if (c == '~' && i < end) { // possibly, quote 459 return _parseQuotedTail(input, i); 460 } 461 // otherwise, loop on 462 } 463 // end of the road, no escapes 464 return new JsonPointer(input, input.substring(1), EMPTY); 465 } 466 467 /** 468 * Method called to parse tail of pointer path, when a potentially 469 * escaped character has been seen. 470 * 471 * @param input Full input for the tail being parsed 472 * @param i Offset to character after tilde 473 */ _parseQuotedTail(String input, int i)474 protected static JsonPointer _parseQuotedTail(String input, int i) { 475 final int end = input.length(); 476 StringBuilder sb = new StringBuilder(Math.max(16, end)); 477 if (i > 2) { 478 sb.append(input, 1, i-1); 479 } 480 _appendEscape(sb, input.charAt(i++)); 481 while (i < end) { 482 char c = input.charAt(i); 483 if (c == '/') { // end is nigh! 484 return new JsonPointer(input, sb.toString(), 485 _parseTail(input.substring(i))); 486 } 487 ++i; 488 if (c == '~' && i < end) { 489 _appendEscape(sb, input.charAt(i++)); 490 continue; 491 } 492 sb.append(c); 493 } 494 // end of the road, last segment 495 return new JsonPointer(input, sb.toString(), EMPTY); 496 } 497 _constructHead()498 protected JsonPointer _constructHead() 499 { 500 // ok; find out who we are to drop 501 JsonPointer last = last(); 502 if (last == this) { 503 return EMPTY; 504 } 505 // and from that, length of suffix to drop 506 int suffixLength = last._asString.length(); 507 JsonPointer next = _nextSegment; 508 return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), _matchingPropertyName, 509 _matchingElementIndex, next._constructHead(suffixLength, last)); 510 } 511 _constructHead(int suffixLength, JsonPointer last)512 protected JsonPointer _constructHead(int suffixLength, JsonPointer last) 513 { 514 if (this == last) { 515 return EMPTY; 516 } 517 JsonPointer next = _nextSegment; 518 String str = _asString; 519 return new JsonPointer(str.substring(0, str.length() - suffixLength), _matchingPropertyName, 520 _matchingElementIndex, next._constructHead(suffixLength, last)); 521 } 522 _appendEscape(StringBuilder sb, char c)523 private static void _appendEscape(StringBuilder sb, char c) { 524 if (c == '0') { 525 c = '~'; 526 } else if (c == '1') { 527 c = '/'; 528 } else { 529 sb.append('~'); 530 } 531 sb.append(c); 532 } 533 } 534