11package com .networknt .schema ;
22
33import java .util .function .Function ;
4+ import java .util .function .IntPredicate ;
45
56/**
67 * Enumeration defining the different approached available to generate the paths added to validation messages.
@@ -11,27 +12,38 @@ public enum PathType {
1112 * The legacy approach, loosely based on JSONPath (but not guaranteed to give valid JSONPath expressions).
1213 */
1314 LEGACY ("$" , (token ) -> "." + token , (index ) -> "[" + index + "]" ),
15+
1416 /**
1517 * Paths as JSONPath expressions.
1618 */
1719 JSON_PATH ("$" , (token ) -> {
20+
21+ if (token .isEmpty ()) {
22+ throw new IllegalArgumentException ("A JSONPath selector cannot be empty" );
23+ }
24+
25+ String t = token ;
1826 /*
1927 * Accepted characters for shorthand paths:
2028 * - 'a' through 'z'
2129 * - 'A' through 'Z'
2230 * - '0' through '9'
2331 * - Underscore ('_')
32+ * - any non-ASCII Unicode character
2433 */
25- if (token .codePoints ().allMatch (c -> (c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' ) || (c >= '0' && c <= '9' ) || c == '_' )) {
26- return "." + token ;
27- } else {
28- if (token .indexOf ('\"' ) != -1 ) {
29- // Make sure also any double quotes are escaped.
30- token = token .replace ("\" " , "\\ \" " );
31- }
32- return "[\" " + token + "\" ]" ;
34+ if (JSONPath .isShorthand (t )) {
35+ return "." + t ;
3336 }
37+
38+ boolean containsApostrophe = 0 <= t .indexOf ('\'' );
39+ if (containsApostrophe ) {
40+ // Make sure also any apostrophes are escaped.
41+ t = t .replace ("'" , "\\ '" );
42+ }
43+
44+ return "['" + token + "']" ;
3445 }, (index ) -> "[" + index + "]" ),
46+
3547 /**
3648 * Paths as JSONPointer expressions.
3749 */
@@ -77,7 +89,7 @@ public enum PathType {
7789 * @return The resulting complete path.
7890 */
7991 public String append (String currentPath , String child ) {
80- return currentPath + appendTokenFn .apply (child );
92+ return currentPath + this . appendTokenFn .apply (child );
8193 }
8294
8395 /**
@@ -88,7 +100,7 @@ public String append(String currentPath, String child) {
88100 * @return The resulting complete path.
89101 */
90102 public String append (String currentPath , int index ) {
91- return currentPath + appendIndexFn .apply (index );
103+ return currentPath + this . appendIndexFn .apply (index );
92104 }
93105
94106 /**
@@ -97,22 +109,137 @@ public String append(String currentPath, int index) {
97109 * @return The root token.
98110 */
99111 public String getRoot () {
100- return rootToken ;
112+ return this . rootToken ;
101113 }
102114
103115 public String convertToJsonPointer (String path ) {
104116 switch (this ) {
105117 case JSON_POINTER : return path ;
106- default : return fromLegacyOrJsonPath (path );
118+ case JSON_PATH : return fromJsonPath (path );
119+ default : return fromLegacy (path );
107120 }
108121 }
109122
110- static String fromLegacyOrJsonPath (String path ) {
123+ static String fromLegacy (String path ) {
111124 return path
112- .replace ("\" " , "" )
113- .replace ("]" , "" )
114- .replace ('[' , '/' )
115- .replace ('.' , '/' )
116- .replace ("$" , "" );
125+ .replace ("\" " , "" )
126+ .replace ("]" , "" )
127+ .replace ('[' , '/' )
128+ .replace ('.' , '/' )
129+ .replace ("$" , "" );
130+ }
131+
132+ static String fromJsonPath (String str ) {
133+ if (null == str || str .isEmpty () || '$' != str .charAt (0 )) {
134+ throw new IllegalArgumentException ("JSON Path must start with '$'" );
135+ }
136+
137+ String tail = str .substring (1 );
138+ if (tail .isEmpty ()) {
139+ return "" ;
140+ }
141+
142+ int len = tail .length ();
143+ StringBuilder sb = new StringBuilder (len );
144+ for (int i = 0 ; i < len ;) {
145+ char c = tail .charAt (i );
146+ switch (c ) {
147+ case '.' : sb .append ('/' ); i = parseShorthand (sb , tail , i + 1 ); break ;
148+ case '[' : sb .append ('/' ); i = parseSelector (sb , tail , i + 1 ); break ;
149+ default : throw new IllegalArgumentException ("JSONPath must reference a property or array index" );
150+ }
151+ }
152+ return sb .toString ();
153+ }
154+
155+ /**
156+ * Parses a JSONPath shorthand selector
157+ * @param sb receives the result
158+ * @param s the source string
159+ * @param pos the index into s immediately following the dot
160+ * @return the index following the selector name
161+ */
162+ static int parseShorthand (StringBuilder sb , String s , int pos ) {
163+ int len = s .length ();
164+ int i = pos ;
165+ for (; i < len ; ++i ) {
166+ char c = s .charAt (i );
167+ switch (c ) {
168+ case '.' :
169+ case '[' :
170+ break ;
171+ default :
172+ sb .append (c );
173+ break ;
174+ }
175+ }
176+ return i ;
177+ }
178+
179+ /**
180+ * Parses a JSONPath selector
181+ * @param sb receives the result
182+ * @param s the source string
183+ * @param pos the index into s immediately following the open bracket
184+ * @return the index following the closing bracket
185+ */
186+ static int parseSelector (StringBuilder sb , String s , int pos ) {
187+ int close = s .indexOf (']' , pos );
188+ if (-1 == close ) {
189+ throw new IllegalArgumentException ("JSONPath contains an unterminated selector" );
190+ }
191+
192+ if ('\'' == s .charAt (pos )) {
193+ parseQuote (sb , s , pos + 1 );
194+ } else {
195+ sb .append (s .substring (pos , close ));
196+ }
197+
198+ return close + 1 ;
199+ }
200+
201+ /**
202+ * Parses a single-quoted string.
203+ * @param sb receives the result
204+ * @param s the source string
205+ * @param pos the index into s immediately following the open quote
206+ * @return the index following the closing quote
207+ */
208+ static int parseQuote (StringBuilder sb , String s , int pos ) {
209+ int close = pos ;
210+ do {
211+ close = s .indexOf ('\'' , close );
212+ if (-1 == close ) {
213+ throw new IllegalArgumentException ("JSONPath contains an unterminated quoted string" );
214+ }
215+ } while ('\\' == s .charAt (close - 1 )) ;
216+ sb .append (s .substring (pos , close ));
217+ return close + 1 ;
218+ }
219+
220+ static class JSONPath {
221+ public static final IntPredicate ALPHA = c -> (c >= 'a' && c <= 'z' ) || (c >= 'A' && c <= 'Z' );
222+ public static final IntPredicate DIGIT = c -> c >= '0' && c <= '9' ;
223+ public static final IntPredicate NON_ASCII = c -> (c >= 0x80 && c <= 0x10FFFF );
224+ public static final IntPredicate UNDERSCORE = c -> '_' == c ;
225+
226+ public static final IntPredicate NAME_FIRST = ALPHA .or (UNDERSCORE ).or (NON_ASCII );
227+ public static final IntPredicate NAME_CHAR = NAME_FIRST .or (DIGIT );
228+
229+ public static boolean isShorthand (String selector ) {
230+ if (null == selector || selector .isEmpty ()) {
231+ throw new IllegalArgumentException ("A JSONPath selector cannot be empty" );
232+ }
233+
234+ /*
235+ * Accepted characters for shorthand paths:
236+ * - 'a' through 'z'
237+ * - 'A' through 'Z'
238+ * - '0' through '9'
239+ * - Underscore ('_')
240+ * - any non-ASCII Unicode character
241+ */
242+ return NAME_FIRST .test (selector .codePointAt (0 )) && selector .codePoints ().skip (1 ).allMatch (NAME_CHAR );
243+ }
117244 }
118245}
0 commit comments