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