1
1
package com .networknt .schema ;
2
2
3
3
import java .util .function .Function ;
4
+ import java .util .function .IntPredicate ;
4
5
5
6
/**
6
7
* Enumeration defining the different approached available to generate the paths added to validation messages.
@@ -11,27 +12,38 @@ public enum PathType {
11
12
* The legacy approach, loosely based on JSONPath (but not guaranteed to give valid JSONPath expressions).
12
13
*/
13
14
LEGACY ("$" , (token ) -> "." + token , (index ) -> "[" + index + "]" ),
15
+
14
16
/**
15
17
* Paths as JSONPath expressions.
16
18
*/
17
19
JSON_PATH ("$" , (token ) -> {
20
+
21
+ if (token .isEmpty ()) {
22
+ throw new IllegalArgumentException ("A JSONPath selector cannot be empty" );
23
+ }
24
+
25
+ String t = token ;
18
26
/*
19
27
* Accepted characters for shorthand paths:
20
28
* - 'a' through 'z'
21
29
* - 'A' through 'Z'
22
30
* - '0' through '9'
23
31
* - Underscore ('_')
32
+ * - any non-ASCII Unicode character
24
33
*/
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 ;
33
36
}
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 + "']" ;
34
45
}, (index ) -> "[" + index + "]" ),
46
+
35
47
/**
36
48
* Paths as JSONPointer expressions.
37
49
*/
@@ -77,7 +89,7 @@ public enum PathType {
77
89
* @return The resulting complete path.
78
90
*/
79
91
public String append (String currentPath , String child ) {
80
- return currentPath + appendTokenFn .apply (child );
92
+ return currentPath + this . appendTokenFn .apply (child );
81
93
}
82
94
83
95
/**
@@ -88,7 +100,7 @@ public String append(String currentPath, String child) {
88
100
* @return The resulting complete path.
89
101
*/
90
102
public String append (String currentPath , int index ) {
91
- return currentPath + appendIndexFn .apply (index );
103
+ return currentPath + this . appendIndexFn .apply (index );
92
104
}
93
105
94
106
/**
@@ -97,22 +109,137 @@ public String append(String currentPath, int index) {
97
109
* @return The root token.
98
110
*/
99
111
public String getRoot () {
100
- return rootToken ;
112
+ return this . rootToken ;
101
113
}
102
114
103
115
public String convertToJsonPointer (String path ) {
104
116
switch (this ) {
105
117
case JSON_POINTER : return path ;
106
- default : return fromLegacyOrJsonPath (path );
118
+ case JSON_PATH : return fromJsonPath (path );
119
+ default : return fromLegacy (path );
107
120
}
108
121
}
109
122
110
- static String fromLegacyOrJsonPath (String path ) {
123
+ static String fromLegacy (String path ) {
111
124
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
+ }
117
244
}
118
245
}
0 commit comments