blob: ad79515cf96bfb310b8cbcbb1ac26a266bd388ac [file] [log] [blame]
Tatu Salorantabb639972013-08-30 21:45:13 -07001package com.fasterxml.jackson.core;
2
3import 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
Tatu Saloranta911cca02013-10-08 10:03:44 -070010 * 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).
Tatu Salorantabb639972013-08-30 21:45:13 -070013 *<p>
Tatu Salorantaf0081302017-03-21 17:07:00 -070014 * Instances are fully immutable and can be cached, shared between threads.
Tatu Salorantabb639972013-08-30 21:45:13 -070015 *
16 * @author Tatu Saloranta
Cowtowncoderd61541d2014-12-09 14:16:11 -080017 *
Tatu Salorantabb639972013-08-30 21:45:13 -070018 * @since 2.3
19 */
20public class JsonPointer
21{
22 /**
Dave Jarvisf35f7d72016-09-21 22:48:27 -070023 * Character used to separate segments.
Tatu Saloranta06f401c2016-09-22 10:30:07 -070024 *
25 * @since 2.9
Dave Jarvisf35f7d72016-09-21 22:48:27 -070026 */
27 public final static char SEPARATOR = '/';
Tatu Saloranta06f401c2016-09-22 10:30:07 -070028
Dave Jarvisf35f7d72016-09-21 22:48:27 -070029 /**
Tatu Salorantabb639972013-08-30 21:45:13 -070030 * Marker instance used to represent segment that matches current
Cowtowncoder6d8e5e62014-12-09 13:47:18 -080031 * node or position (that is, returns true for
32 * {@link #matches()}).
Tatu Salorantabb639972013-08-30 21:45:13 -070033 */
34 protected final static JsonPointer EMPTY = new JsonPointer();
35
36 /**
37 * Reference to rest of the pointer beyond currently matching
Cowtowncoder6d8e5e62014-12-09 13:47:18 -080038 * segment (if any); null if this pointer refers to the matching
Tatu Salorantabb639972013-08-30 21:45:13 -070039 * segment.
40 */
41 protected final JsonPointer _nextSegment;
Alex Soto7384d062014-12-03 11:57:02 +010042
43 /**
Cowtowncoder6d8e5e62014-12-09 13:47:18 -080044 * Reference from currently matching segment (if any) to node
Alex Soto7384d062014-12-03 11:57:02 +010045 * before leaf.
Cowtowncoderd2c60232014-12-09 14:39:17 -080046 * 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).
Cowtowncoderd61541d2014-12-09 14:16:11 -080052 *
53 * @since 2.5
Alex Soto7384d062014-12-03 11:57:02 +010054 */
Cowtowncoderd2c60232014-12-09 14:39:17 -080055 protected volatile JsonPointer _head;
Alex Soto7384d062014-12-03 11:57:02 +010056
Tatu Salorantabb639972013-08-30 21:45:13 -070057 /**
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 /**********************************************************
esanchez7c56b002015-10-27 10:45:25 -070069 /* Construction
Tatu Salorantabb639972013-08-30 21:45:13 -070070 /**********************************************************
71 */
72
73 /**
74 * Constructor used for creating "empty" instance, used to represent
75 * state that matches current node.
76 */
Tatu Saloranta32e4e912014-01-26 19:59:28 -080077 protected JsonPointer() {
Tatu Salorantabb639972013-08-30 21:45:13 -070078 _nextSegment = null;
79 _matchingPropertyName = "";
80 _matchingElementIndex = -1;
81 _asString = "";
82 }
83
84 /**
85 * Constructor used for creating non-empty Segments
86 */
Cowtowncoderd2c60232014-12-09 14:39:17 -080087 protected JsonPointer(String fullString, String segment, JsonPointer next) {
Tatu Salorantabb639972013-08-30 21:45:13 -070088 _asString = fullString;
89 _nextSegment = next;
90 // Ok; may always be a property
91 _matchingPropertyName = segment;
Cowtowncoderd2c60232014-12-09 14:39:17 -080092 // but could be an index, if parsable
Tatu Saloranta4769c362014-03-14 13:26:30 -070093 _matchingElementIndex = _parseIndex(segment);
Tatu Salorantabb639972013-08-30 21:45:13 -070094 }
Cowtowncoder17628142014-12-09 15:03:38 -080095
96 /**
97 * @since 2.5
98 */
99 protected JsonPointer(String fullString, String segment, int matchIndex, JsonPointer next) {
100 _asString = fullString;
101 _nextSegment = next;
102 _matchingPropertyName = segment;
103 _matchingElementIndex = matchIndex;
104 }
Tatu Salorantaa12195a2013-09-02 21:24:15 -0700105
Tatu Salorantabb639972013-08-30 21:45:13 -0700106 /*
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 */
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800121 public static JsonPointer compile(String input) throws IllegalArgumentException
Tatu Salorantabb639972013-08-30 21:45:13 -0700122 {
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) != '/') {
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800129 throw new IllegalArgumentException("Invalid input: JSON Pointer expression must start with '/': "+"\""+input+"\"");
Tatu Salorantabb639972013-08-30 21:45:13 -0700130 }
Cowtowncoderd2c60232014-12-09 14:39:17 -0800131 return _parseTail(input);
Tatu Salorantabb639972013-08-30 21:45:13 -0700132 }
133
Tatu Salorantaa12195a2013-09-02 21:24:15 -0700134 /**
135 * Alias for {@link #compile}; added to make instances automatically
136 * deserializable by Jackson databind.
137 */
138 public static JsonPointer valueOf(String input) { return compile(input); }
139
Tatu Salorantaf0081302017-03-21 17:07:00 -0700140 /**
141 * Factory method that will construct a pointer instance that describes
142 * path to location given {@link JsonStreamContext} points to.
143 *
144 * @param context Context to build pointer expression fot
145 * @param includeRoot Whether to include number offset for virtual "root context"
146 * or not.
147 *
148 * @since 2.9
149 */
150 public static JsonPointer forPath(JsonStreamContext context,
151 boolean includeRoot)
152 {
153 // First things first: last segment may be for START_ARRAY/START_OBJECT,
154 // in which case it does not yet point to anything, and should be skipped
155 if (context == null) {
156 return EMPTY;
157 }
158 if (!context.hasPathSegment()) {
159 // one special case; do not prune root if we need it
160 if (!(includeRoot && context.inRoot() && context.hasCurrentIndex())) {
161 context = context.getParent();
162 }
163 }
164 JsonPointer tail = null;
165
166 for (; context != null; context = context.getParent()) {
167 if (context.inObject()) {
168 String seg = context.getCurrentName();
169 if (seg == null) { // is this legal?
170 seg = "";
171 }
172 tail = new JsonPointer(_fullPath(tail, seg), seg, tail);
173 } else if (context.inArray() || includeRoot) {
174 int ix = context.getCurrentIndex();
175 String ixStr = String.valueOf(ix);
176 tail = new JsonPointer(_fullPath(tail, ixStr), ixStr, ix, tail);
177 }
178 // NOTE: this effectively drops ROOT node(s); should have 1 such node,
179 // as the last one, but we don't have to care (probably some paths have
180 // no root, for example)
181 }
182 if (tail == null) {
183 return EMPTY;
184 }
185 return tail;
186 }
187
188 private static String _fullPath(JsonPointer tail, String segment)
189 {
190 if (tail == null) {
191 StringBuilder sb = new StringBuilder(segment.length()+1);
192 sb.append('/');
193 _appendEscaped(sb, segment);
194 return sb.toString();
195 }
196 String tailDesc = tail._asString;
197 StringBuilder sb = new StringBuilder(segment.length() + 1 + tailDesc.length());
198 sb.append('/');
199 _appendEscaped(sb, segment);
200 sb.append(tailDesc);
201 return sb.toString();
202 }
203
204 private static void _appendEscaped(StringBuilder sb, String segment)
205 {
206 for (int i = 0, end = segment.length(); i < end; ++i) {
207 char c = segment.charAt(i);
208 if (c == '/') {
209 sb.append("~1");
210 continue;
211 }
212 if (c == '~') {
213 sb.append("~0");
214 continue;
215 }
216 sb.append(c);
217 }
218 }
219
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800220 /* Factory method that composes a pointer instance, given a set
Tatu Salorantabb639972013-08-30 21:45:13 -0700221 * of 'raw' segments: raw meaning that no processing will be done,
222 * no escaping may is present.
223 *
224 * @param segments
225 *
226 * @return Constructed path instance
227 */
228 /* TODO!
229 public static JsonPointer fromSegment(String... segments)
230 {
231 if (segments.length == 0) {
232 return EMPTY;
233 }
234 JsonPointer prev = null;
235
236 for (String segment : segments) {
237 JsonPointer next = new JsonPointer()
238 }
239 }
240 */
241
242 /*
243 /**********************************************************
244 /* Public API
245 /**********************************************************
246 */
247
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800248 public boolean matches() { return _nextSegment == null; }
249 public String getMatchingProperty() { return _matchingPropertyName; }
250 public int getMatchingIndex() { return _matchingElementIndex; }
251 public boolean mayMatchProperty() { return _matchingPropertyName != null; }
252 public boolean mayMatchElement() { return _matchingElementIndex >= 0; }
Tatu Salorantabb639972013-08-30 21:45:13 -0700253
Tatu Salorantae2207342014-12-07 10:11:52 -0800254 /**
Cowtowncoderd61541d2014-12-09 14:16:11 -0800255 * Returns the leaf of current JSON Pointer expression.
256 * Leaf is the last non-null segment of current JSON Pointer.
257 *
258 * @since 2.5
Alex Sotofca3ed32014-12-09 09:53:19 +0100259 */
260 public JsonPointer last() {
261 JsonPointer current = this;
Cowtowncoder17628142014-12-09 15:03:38 -0800262 if (current == EMPTY) {
263 return null;
264 }
265 JsonPointer next;
266 while ((next = current._nextSegment) != JsonPointer.EMPTY) {
Cowtowncoderd61541d2014-12-09 14:16:11 -0800267 current = next;
Alex Sotofca3ed32014-12-09 09:53:19 +0100268 }
269 return current;
270 }
271
Tatu Salorantaf0081302017-03-21 17:07:00 -0700272 /**
273 * Mutant factory method that will return
274 *<ul>
275 * <li>`tail` if `this` instance is "empty" pointer, OR
276 * </li>
277 * <li>`this` instance if `tail` is "empty" pointer, OR
278 * </li>
279 * <li>Newly constructed {@link JsonPointer} instance that starts with all segments
280 * of `this`, followed by all segments of `tail`.
281 * </li>
282 *</ul>
283 *
284 * @param tail {@link JsonPointer} instance to append to this one, to create a new pointer instance
285 *
286 * @return Either `this` instance, `tail`, or a newly created combination, as per description above.
287 */
Cowtowncoder17628142014-12-09 15:03:38 -0800288 public JsonPointer append(JsonPointer tail) {
289 if (this == EMPTY) {
290 return tail;
291 }
292 if (tail == EMPTY) {
293 return this;
294 }
Tatu Salorantaf0081302017-03-21 17:07:00 -0700295 // 21-Mar-2017, tatu: Not superbly efficient; could probably improve by not concatenating,
296 // re-decoding -- by stitching together segments -- but for now should be fine.
297
Alex Sotoe1dffcc2014-12-09 12:53:55 +0100298 String currentJsonPointer = _asString;
Cowtowncoder17628142014-12-09 15:03:38 -0800299 if (currentJsonPointer.endsWith("/")) {
Alex Sotoe1dffcc2014-12-09 12:53:55 +0100300 //removes final slash
301 currentJsonPointer = currentJsonPointer.substring(0, currentJsonPointer.length()-1);
302 }
Cowtowncoder17628142014-12-09 15:03:38 -0800303 return compile(currentJsonPointer + tail._asString);
Alex Sotoe1dffcc2014-12-09 12:53:55 +0100304 }
305
Alex Sotofca3ed32014-12-09 09:53:19 +0100306 /**
Tatu Salorantae2207342014-12-07 10:11:52 -0800307 * Method that may be called to see if the pointer would match property
308 * (of a JSON Object) with given name.
309 *
310 * @since 2.5
311 */
312 public boolean matchesProperty(String name) {
313 return (_nextSegment != null) && _matchingPropertyName.equals(name);
314 }
315
Tatu Salorantabb639972013-08-30 21:45:13 -0700316 public JsonPointer matchProperty(String name) {
Cowtowncoderb4b7a7e2015-04-09 17:43:06 -0700317 if ((_nextSegment != null) && _matchingPropertyName.equals(name)) {
318 return _nextSegment;
Tatu Salorantabb639972013-08-30 21:45:13 -0700319 }
Cowtowncoderb4b7a7e2015-04-09 17:43:06 -0700320 return null;
Tatu Salorantabb639972013-08-30 21:45:13 -0700321 }
322
Tatu Salorantae2207342014-12-07 10:11:52 -0800323 /**
324 * Method that may be called to see if the pointer would match
325 * array element (of a JSON Array) with given index.
326 *
327 * @since 2.5
328 */
329 public boolean matchesElement(int index) {
330 return (index == _matchingElementIndex) && (index >= 0);
Tatu Salorantabb639972013-08-30 21:45:13 -0700331 }
Tatu Saloranta2d1e19e2013-09-03 20:40:03 -0700332
333 /**
Cowtowncoderb4b7a7e2015-04-09 17:43:06 -0700334 * @since 2.6
335 */
336 public JsonPointer matchElement(int index) {
337 if ((index != _matchingElementIndex) || (index < 0)) {
338 return null;
339 }
340 return _nextSegment;
341 }
342
343 /**
Tatu Saloranta2d1e19e2013-09-03 20:40:03 -0700344 * Accessor for getting a "sub-pointer", instance where current segment
Tatu Salorantae2207342014-12-07 10:11:52 -0800345 * has been removed and pointer includes rest of segments.
346 * For matching state, will return null.
Tatu Saloranta2d1e19e2013-09-03 20:40:03 -0700347 */
348 public JsonPointer tail() {
349 return _nextSegment;
350 }
Alex Soto7384d062014-12-03 11:57:02 +0100351
352 /**
Cowtowncoderd61541d2014-12-09 14:16:11 -0800353 * Accessor for getting a pointer instance that is identical to this
354 * instance except that the last segment has been dropped.
355 * For example, for JSON Point "/root/branch/leaf", this method would
356 * return pointer "/root/branch" (compared to {@link #tail()} that
357 * would return "/branch/leaf").
358 * For leaf
Tatu Salorantae2207342014-12-07 10:11:52 -0800359 *
360 * @since 2.5
Alex Soto7384d062014-12-03 11:57:02 +0100361 */
362 public JsonPointer head() {
Cowtowncoderd2c60232014-12-09 14:39:17 -0800363 JsonPointer h = _head;
364 if (h == null) {
365 if (this != EMPTY) {
366 h = _constructHead();
367 }
368 _head = h;
369 }
370 return h;
Alex Soto7384d062014-12-03 11:57:02 +0100371 }
372
Tatu Salorantabb639972013-08-30 21:45:13 -0700373 /*
374 /**********************************************************
375 /* Standard method overrides
376 /**********************************************************
377 */
378
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800379 @Override public String toString() { return _asString; }
380 @Override public int hashCode() { return _asString.hashCode(); }
Tatu Salorantabb639972013-08-30 21:45:13 -0700381
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800382 @Override public boolean equals(Object o) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700383 if (o == this) return true;
384 if (o == null) return false;
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800385 if (!(o instanceof JsonPointer)) return false;
Tatu Salorantabb639972013-08-30 21:45:13 -0700386 return _asString.equals(((JsonPointer) o)._asString);
387 }
388
389 /*
390 /**********************************************************
391 /* Internal methods
392 /**********************************************************
393 */
394
Tatu Saloranta4769c362014-03-14 13:26:30 -0700395 private final static int _parseIndex(String str) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700396 final int len = str.length();
Cowtowncoderd9995142014-12-30 17:09:51 -0800397 // [core#133]: beware of super long indexes; assume we never
Tatu Saloranta4769c362014-03-14 13:26:30 -0700398 // have arrays over 2 billion entries so ints are fine.
399 if (len == 0 || len > 10) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700400 return -1;
401 }
Cowtowncoderd9995142014-12-30 17:09:51 -0800402 // [core#176]: no leading zeroes allowed
403 char c = str.charAt(0);
404 if (c <= '0') {
405 return (len == 1 && c == '0') ? 0 : -1;
406 }
407 if (c > '9') {
408 return -1;
409 }
410 for (int i = 1; i < len; ++i) {
411 c = str.charAt(i);
Tatu Salorantabb639972013-08-30 21:45:13 -0700412 if (c > '9' || c < '0') {
413 return -1;
414 }
415 }
Tatu Saloranta4769c362014-03-14 13:26:30 -0700416 if (len == 10) {
417 long l = NumberInput.parseLong(str);
418 if (l > Integer.MAX_VALUE) {
419 return -1;
420 }
421 }
Tatu Salorantabb639972013-08-30 21:45:13 -0700422 return NumberInput.parseInt(str);
423 }
424
Cowtowncoderd2c60232014-12-09 14:39:17 -0800425 protected static JsonPointer _parseTail(String input) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700426 final int end = input.length();
427
428 // first char is the contextual slash, skip
Tatu Saloranta240c4552013-09-03 21:14:50 -0700429 for (int i = 1; i < end; ) {
430 char c = input.charAt(i);
Tatu Salorantabb639972013-08-30 21:45:13 -0700431 if (c == '/') { // common case, got a segment
Cowtowncoder6d8e5e62014-12-09 13:47:18 -0800432 return new JsonPointer(input, input.substring(1, i),
Cowtowncoderd2c60232014-12-09 14:39:17 -0800433 _parseTail(input.substring(i)));
Tatu Salorantabb639972013-08-30 21:45:13 -0700434 }
Tatu Saloranta240c4552013-09-03 21:14:50 -0700435 ++i;
Tatu Salorantabb639972013-08-30 21:45:13 -0700436 // quoting is different; offline this case
437 if (c == '~' && i < end) { // possibly, quote
Cowtowncoderd2c60232014-12-09 14:39:17 -0800438 return _parseQuotedTail(input, i);
Tatu Salorantabb639972013-08-30 21:45:13 -0700439 }
440 // otherwise, loop on
441 }
442 // end of the road, no escapes
Cowtowncoderd2c60232014-12-09 14:39:17 -0800443 return new JsonPointer(input, input.substring(1), EMPTY);
Tatu Salorantabb639972013-08-30 21:45:13 -0700444 }
445
446 /**
447 * Method called to parse tail of pointer path, when a potentially
448 * escaped character has been seen.
449 *
450 * @param input Full input for the tail being parsed
451 * @param i Offset to character after tilde
452 */
Cowtowncoderd2c60232014-12-09 14:39:17 -0800453 protected static JsonPointer _parseQuotedTail(String input, int i) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700454 final int end = input.length();
455 StringBuilder sb = new StringBuilder(Math.max(16, end));
456 if (i > 2) {
457 sb.append(input, 1, i-1);
458 }
459 _appendEscape(sb, input.charAt(i++));
460 while (i < end) {
Tatu Saloranta240c4552013-09-03 21:14:50 -0700461 char c = input.charAt(i);
Tatu Salorantabb639972013-08-30 21:45:13 -0700462 if (c == '/') { // end is nigh!
Cowtowncoder6d8e5e62014-12-09 13:47:18 -0800463 return new JsonPointer(input, sb.toString(),
Cowtowncoderd2c60232014-12-09 14:39:17 -0800464 _parseTail(input.substring(i)));
Tatu Salorantabb639972013-08-30 21:45:13 -0700465 }
Tatu Saloranta240c4552013-09-03 21:14:50 -0700466 ++i;
Tatu Salorantabb639972013-08-30 21:45:13 -0700467 if (c == '~' && i < end) {
Tatu Saloranta240c4552013-09-03 21:14:50 -0700468 _appendEscape(sb, input.charAt(i++));
Tatu Salorantabb639972013-08-30 21:45:13 -0700469 continue;
470 }
471 sb.append(c);
472 }
473 // end of the road, last segment
Cowtowncoderd2c60232014-12-09 14:39:17 -0800474 return new JsonPointer(input, sb.toString(), EMPTY);
475 }
476
477 protected JsonPointer _constructHead()
478 {
Cowtowncoder17628142014-12-09 15:03:38 -0800479 // ok; find out who we are to drop
480 JsonPointer last = last();
481 if (last == this) {
Cowtowncoderd2c60232014-12-09 14:39:17 -0800482 return EMPTY;
483 }
Cowtowncoder17628142014-12-09 15:03:38 -0800484 // and from that, length of suffix to drop
485 int suffixLength = last._asString.length();
486 JsonPointer next = _nextSegment;
487 return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), _matchingPropertyName,
488 _matchingElementIndex, next._constructHead(suffixLength, last));
489 }
490
491 protected JsonPointer _constructHead(int suffixLength, JsonPointer last)
492 {
493 if (this == last) {
494 return EMPTY;
495 }
496 JsonPointer next = _nextSegment;
497 String str = _asString;
498 return new JsonPointer(str.substring(0, str.length() - suffixLength), _matchingPropertyName,
499 _matchingElementIndex, next._constructHead(suffixLength, last));
Tatu Salorantabb639972013-08-30 21:45:13 -0700500 }
501
Tatu Saloranta32e4e912014-01-26 19:59:28 -0800502 private static void _appendEscape(StringBuilder sb, char c) {
Tatu Salorantabb639972013-08-30 21:45:13 -0700503 if (c == '0') {
504 c = '~';
Tatu Saloranta240c4552013-09-03 21:14:50 -0700505 } else if (c == '1') {
Tatu Saloranta11151c52016-09-30 15:20:14 -0700506 c = '/';
Tatu Salorantabb639972013-08-30 21:45:13 -0700507 } else {
508 sb.append('~');
509 }
510 sb.append(c);
511 }
512}