Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||||||
JSONTokener |
|
| 6.0;6 |
1 | package org.apache.tapestry.json; |
|
2 | ||
3 | /* |
|
4 | Copyright (c) 2002 JSON.org |
|
5 | ||
6 | Permission is hereby granted, free of charge, to any person obtaining a copy |
|
7 | of this software and associated documentation files (the "Software"), to deal |
|
8 | in the Software without restriction, including without limitation the rights |
|
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
10 | copies of the Software, and to permit persons to whom the Software is |
|
11 | furnished to do so, subject to the following conditions: |
|
12 | ||
13 | The above copyright notice and this permission notice shall be included in all |
|
14 | copies or substantial portions of the Software. |
|
15 | ||
16 | The Software shall be used for Good, not Evil. |
|
17 | ||
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
24 | SOFTWARE. |
|
25 | */ |
|
26 | ||
27 | import java.text.ParseException; |
|
28 | ||
29 | /** |
|
30 | * A JSONTokener takes a source string and extracts characters and tokens from |
|
31 | * it. It is used by the JSONObject and JSONArray constructors to parse JSON |
|
32 | * source strings. |
|
33 | * |
|
34 | * <p/>The toString() method has been modified from its original form to provide |
|
35 | * easier to understand exception reporting. |
|
36 | * |
|
37 | * @author JSON.org, jkuhnert |
|
38 | * @version 1 |
|
39 | */ |
|
40 | public class JSONTokener |
|
41 | { |
|
42 | ||
43 | /** |
|
44 | * The index of the next character. |
|
45 | */ |
|
46 | private int myIndex; |
|
47 | ||
48 | /** |
|
49 | * The source string being tokenized. |
|
50 | */ |
|
51 | private String mySource; |
|
52 | ||
53 | /** |
|
54 | * Construct a JSONTokener from a string. |
|
55 | * |
|
56 | * @param s |
|
57 | * A source string. |
|
58 | */ |
|
59 | public JSONTokener(String s) |
|
60 | 1 | { |
61 | 1 | this.myIndex = 0; |
62 | 1 | this.mySource = s; |
63 | 1 | } |
64 | ||
65 | /** |
|
66 | * Back up one character. This provides a sort of lookahead capability, so |
|
67 | * that you can test for a digit or letter before attempting to parse the |
|
68 | * next number or identifier. |
|
69 | */ |
|
70 | public void back() |
|
71 | { |
|
72 | 3 | if (this.myIndex > 0) |
73 | { |
|
74 | 3 | this.myIndex -= 1; |
75 | } |
|
76 | 3 | } |
77 | ||
78 | /** |
|
79 | * Get the hex value of a character (base16). |
|
80 | * |
|
81 | * @param c |
|
82 | * A character between '0' and '9' or between 'A' and 'F' or |
|
83 | * between 'a' and 'f'. |
|
84 | * @return An int between 0 and 15, or -1 if c was not a hex digit. |
|
85 | */ |
|
86 | public static int dehexchar(char c) |
|
87 | { |
|
88 | 0 | if (c >= '0' && c <= '9') { return c - '0'; } |
89 | 0 | if (c >= 'A' && c <= 'F') { return c + 10 - 'A'; } |
90 | 0 | if (c >= 'a' && c <= 'f') { return c + 10 - 'a'; } |
91 | 0 | return -1; |
92 | } |
|
93 | ||
94 | /** |
|
95 | * Determine if the source string still contains characters that next() can |
|
96 | * consume. |
|
97 | * |
|
98 | * @return true if not yet at the end of the source. |
|
99 | */ |
|
100 | public boolean more() |
|
101 | { |
|
102 | 23 | return this.myIndex < this.mySource.length(); |
103 | } |
|
104 | ||
105 | /** |
|
106 | * Get the next character in the source string. |
|
107 | * |
|
108 | * @return The next character, or 0 if past the end of the source string. |
|
109 | */ |
|
110 | public char next() |
|
111 | { |
|
112 | 23 | if (more()) |
113 | { |
|
114 | 22 | char c = this.mySource.charAt(this.myIndex); |
115 | 22 | this.myIndex += 1; |
116 | 22 | return c; |
117 | } |
|
118 | 1 | return 0; |
119 | } |
|
120 | ||
121 | /** |
|
122 | * Consume the next character, and check that it matches a specified |
|
123 | * character. |
|
124 | * |
|
125 | * @param c |
|
126 | * The character to match. |
|
127 | * @return The character. |
|
128 | * @throws ParseException |
|
129 | * if the character does not match. |
|
130 | */ |
|
131 | public char next(char c) |
|
132 | throws ParseException |
|
133 | { |
|
134 | 0 | char n = next(); |
135 | 0 | if (n != c) { throw syntaxError("Expected '" + c + "' and instead saw '" + n + "'."); } |
136 | 0 | return n; |
137 | } |
|
138 | ||
139 | /** |
|
140 | * Get the next n characters. |
|
141 | * |
|
142 | * @param n |
|
143 | * The number of characters to take. |
|
144 | * @return A string of n characters. |
|
145 | * @exception ParseException |
|
146 | * Substring bounds error if there are not n characters |
|
147 | * remaining in the source string. |
|
148 | */ |
|
149 | public String next(int n) |
|
150 | throws ParseException |
|
151 | { |
|
152 | 0 | int i = this.myIndex; |
153 | 0 | int j = i + n; |
154 | 0 | if (j >= this.mySource.length()) { throw syntaxError("Substring bounds error"); } |
155 | 0 | this.myIndex += n; |
156 | 0 | return this.mySource.substring(i, j); |
157 | } |
|
158 | ||
159 | /** |
|
160 | * Get the next char in the string, skipping whitespace and comments |
|
161 | * (slashslash, slashstar, and hash). |
|
162 | * |
|
163 | * @throws ParseException |
|
164 | * @return A character, or 0 if there are no more characters. |
|
165 | */ |
|
166 | public char nextClean() |
|
167 | throws java.text.ParseException |
|
168 | { |
|
169 | while(true) |
|
170 | { |
|
171 | 6 | char c = next(); |
172 | 6 | if (c == '/') |
173 | { |
|
174 | 0 | switch(next()) |
175 | { |
|
176 | case '/': |
|
177 | do |
|
178 | { |
|
179 | 0 | c = next(); |
180 | 0 | } while(c != '\n' && c != '\r' && c != 0); |
181 | 0 | break; |
182 | case '*': |
|
183 | while(true) |
|
184 | { |
|
185 | 0 | c = next(); |
186 | 0 | if (c == 0) { throw syntaxError("Unclosed comment."); } |
187 | 0 | if (c == '*') |
188 | { |
|
189 | 0 | if (next() == '/') |
190 | { |
|
191 | 0 | break; |
192 | } |
|
193 | 0 | back(); |
194 | } |
|
195 | } |
|
196 | break; |
|
197 | default: |
|
198 | 0 | back(); |
199 | 0 | return '/'; |
200 | } |
|
201 | } |
|
202 | 6 | else if (c == '#') |
203 | { |
|
204 | do |
|
205 | { |
|
206 | 0 | c = next(); |
207 | 0 | } while(c != '\n' && c != '\r' && c != 0); |
208 | } |
|
209 | 6 | else if (c == 0 || c > ' ') { return c; } |
210 | 0 | } |
211 | } |
|
212 | ||
213 | /** |
|
214 | * Return the characters up to the next close quote character. Backslash |
|
215 | * processing is done. The formal JSON format does not allow strings in |
|
216 | * single quotes, but an implementation is allowed to accept them. |
|
217 | * |
|
218 | * @param quote |
|
219 | * The quoting character, either <code>"</code> <small>(double |
|
220 | * quote)</small> or <code>'</code> <small>(single |
|
221 | * quote)</small>. |
|
222 | * @return A String. |
|
223 | * @exception ParseException |
|
224 | * Unterminated string. |
|
225 | */ |
|
226 | public String nextString(char quote) |
|
227 | throws ParseException |
|
228 | { |
|
229 | char c; |
|
230 | 0 | StringBuffer sb = new StringBuffer(); |
231 | while(true) |
|
232 | { |
|
233 | 0 | c = next(); |
234 | 0 | switch(c) |
235 | { |
|
236 | case 0: |
|
237 | case '\n': |
|
238 | case '\r': |
|
239 | 0 | throw syntaxError("Unterminated string"); |
240 | case '\\': |
|
241 | 0 | c = next(); |
242 | 0 | switch(c) |
243 | { |
|
244 | case 'b': |
|
245 | 0 | sb.append('\b'); |
246 | 0 | break; |
247 | case 't': |
|
248 | 0 | sb.append('\t'); |
249 | 0 | break; |
250 | case 'n': |
|
251 | 0 | sb.append('\n'); |
252 | 0 | break; |
253 | case 'f': |
|
254 | 0 | sb.append('\f'); |
255 | 0 | break; |
256 | case 'r': |
|
257 | 0 | sb.append('\r'); |
258 | 0 | break; |
259 | case 'u': |
|
260 | 0 | sb.append((char) Integer.parseInt(next(4), 16)); |
261 | 0 | break; |
262 | case 'x': |
|
263 | 0 | sb.append((char) Integer.parseInt(next(2), 16)); |
264 | 0 | break; |
265 | default: |
|
266 | 0 | sb.append(c); |
267 | } |
|
268 | 0 | break; |
269 | default: |
|
270 | 0 | if (c == quote) { return sb.toString(); } |
271 | 0 | sb.append(c); |
272 | } |
|
273 | } |
|
274 | } |
|
275 | ||
276 | /** |
|
277 | * Get the text up but not including the specified character or the end of |
|
278 | * line, whichever comes first. |
|
279 | * |
|
280 | * @param d |
|
281 | * A delimiter character. |
|
282 | * @return A string. |
|
283 | */ |
|
284 | public String nextTo(char d) |
|
285 | { |
|
286 | 0 | StringBuffer sb = new StringBuffer(); |
287 | while(true) |
|
288 | { |
|
289 | 0 | char c = next(); |
290 | 0 | if (c == d || c == 0 || c == '\n' || c == '\r') |
291 | { |
|
292 | 0 | if (c != 0) |
293 | { |
|
294 | 0 | back(); |
295 | } |
|
296 | 0 | return sb.toString().trim(); |
297 | } |
|
298 | 0 | sb.append(c); |
299 | 0 | } |
300 | } |
|
301 | ||
302 | /** |
|
303 | * Get the text up but not including one of the specified delimeter |
|
304 | * characters or the end of line, whichever comes first. |
|
305 | * |
|
306 | * @param delimiters |
|
307 | * A set of delimiter characters. |
|
308 | * @return A string, trimmed. |
|
309 | */ |
|
310 | public String nextTo(String delimiters) |
|
311 | { |
|
312 | char c; |
|
313 | 0 | StringBuffer sb = new StringBuffer(); |
314 | while(true) |
|
315 | { |
|
316 | 0 | c = next(); |
317 | 0 | if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') |
318 | { |
|
319 | 0 | if (c != 0) |
320 | { |
|
321 | 0 | back(); |
322 | } |
|
323 | 0 | return sb.toString().trim(); |
324 | } |
|
325 | 0 | sb.append(c); |
326 | } |
|
327 | } |
|
328 | ||
329 | /** |
|
330 | * Get the next value. The value can be a Boolean, Double, Integer, |
|
331 | * JSONArray, JSONObject, or String, or the JSONObject.NULL object. |
|
332 | * |
|
333 | * @exception ParseException |
|
334 | * The source does not conform to JSON syntax. |
|
335 | * @return An object. |
|
336 | */ |
|
337 | public Object nextValue() |
|
338 | throws ParseException |
|
339 | { |
|
340 | 2 | char c = nextClean(); |
341 | String s; |
|
342 | ||
343 | 2 | switch(c) |
344 | { |
|
345 | case '"': |
|
346 | case '\'': |
|
347 | 0 | return nextString(c); |
348 | case '{': |
|
349 | 0 | back(); |
350 | 0 | return new JSONObject(this); |
351 | case '[': |
|
352 | 0 | back(); |
353 | 0 | return new JSONArray(this); |
354 | } |
|
355 | ||
356 | /* |
|
357 | * Handle unquoted text. This could be the values true, false, or null, |
|
358 | * or it can be a number. An implementation (such as this one) is |
|
359 | * allowed to also accept non-standard forms. Accumulate characters |
|
360 | * until we reach the end of the text or a formatting character. |
|
361 | */ |
|
362 | ||
363 | 2 | StringBuffer sb = new StringBuffer(); |
364 | 2 | char b = c; |
365 | 19 | while(c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) |
366 | { |
|
367 | 17 | sb.append(c); |
368 | 17 | c = next(); |
369 | } |
|
370 | 2 | back(); |
371 | ||
372 | /* |
|
373 | * If it is true, false, or null, return the proper value. |
|
374 | */ |
|
375 | ||
376 | 2 | s = sb.toString().trim(); |
377 | 2 | if (s.equals("")) { throw syntaxError("Missing value."); } |
378 | 2 | if (s.equalsIgnoreCase("true")) { return Boolean.TRUE; } |
379 | 1 | if (s.equalsIgnoreCase("false")) { return Boolean.FALSE; } |
380 | 1 | if (s.equalsIgnoreCase("null")) { return JSONObject.NULL; } |
381 | ||
382 | /* |
|
383 | * If it might be a number, try converting it. We support the 0- and 0x- |
|
384 | * conventions. If a number cannot be produced, then the value will just |
|
385 | * be a string. Note that the 0-, 0x-, plus, and implied string |
|
386 | * conventions are non-standard. A JSON parser is free to accept |
|
387 | * non-JSON forms as long as it accepts all correct JSON forms. |
|
388 | */ |
|
389 | ||
390 | 1 | if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') |
391 | { |
|
392 | 0 | if (b == '0') |
393 | { |
|
394 | 0 | if (s.length() > 2 && (s.charAt(1) == 'x' || s.charAt(1) == 'X')) |
395 | { |
|
396 | try |
|
397 | { |
|
398 | 0 | return new Integer(Integer.parseInt(s.substring(2), 16)); |
399 | } |
|
400 | 0 | catch (Exception e) |
401 | { |
|
402 | /* Ignore the error */ |
|
403 | 0 | } |
404 | } |
|
405 | else |
|
406 | { |
|
407 | try |
|
408 | { |
|
409 | 0 | return new Integer(Integer.parseInt(s, 8)); |
410 | } |
|
411 | 0 | catch (Exception e) |
412 | { |
|
413 | /* Ignore the error */ |
|
414 | } |
|
415 | } |
|
416 | } |
|
417 | try |
|
418 | { |
|
419 | 0 | return new Integer(s); |
420 | } |
|
421 | 0 | catch (Exception e) |
422 | { |
|
423 | /* Ignore the error */ |
|
424 | } |
|
425 | try |
|
426 | { |
|
427 | 0 | return new Double(s); |
428 | } |
|
429 | 0 | catch (Exception e) |
430 | { |
|
431 | /* Ignore the error */ |
|
432 | } |
|
433 | } |
|
434 | 1 | return s; |
435 | } |
|
436 | ||
437 | /** |
|
438 | * Skip characters until the next character is the requested character. If |
|
439 | * the requested character is not found, no characters are skipped. |
|
440 | * |
|
441 | * @param to |
|
442 | * A character to skip to. |
|
443 | * @return The requested character, or zero if the requested character is |
|
444 | * not found. |
|
445 | */ |
|
446 | public char skipTo(char to) |
|
447 | { |
|
448 | char c; |
|
449 | 0 | int index = this.myIndex; |
450 | do |
|
451 | { |
|
452 | 0 | c = next(); |
453 | 0 | if (c == 0) |
454 | { |
|
455 | 0 | this.myIndex = index; |
456 | 0 | return c; |
457 | } |
|
458 | 0 | } while(c != to); |
459 | 0 | back(); |
460 | 0 | return c; |
461 | } |
|
462 | ||
463 | /** |
|
464 | * Skip characters until past the requested string. If it is not found, we |
|
465 | * are left at the end of the source. |
|
466 | * |
|
467 | * @param to |
|
468 | * A string to skip past. |
|
469 | */ |
|
470 | public void skipPast(String to) |
|
471 | { |
|
472 | 0 | this.myIndex = this.mySource.indexOf(to, this.myIndex); |
473 | 0 | if (this.myIndex < 0) |
474 | { |
|
475 | 0 | this.myIndex = this.mySource.length(); |
476 | } |
|
477 | else |
|
478 | { |
|
479 | 0 | this.myIndex += to.length(); |
480 | } |
|
481 | 0 | } |
482 | ||
483 | /** |
|
484 | * Make a ParseException to signal a syntax error. |
|
485 | * |
|
486 | * @param message |
|
487 | * The error message. |
|
488 | * @return A ParseException object, suitable for throwing |
|
489 | */ |
|
490 | public ParseException syntaxError(String message) |
|
491 | { |
|
492 | 1 | return new ParseException(message + toString(), this.myIndex); |
493 | } |
|
494 | ||
495 | /** |
|
496 | * Make a printable string of this JSONTokener. |
|
497 | * |
|
498 | * @return " at character [this.myIndex] of [this.mySource]" |
|
499 | */ |
|
500 | public String toString() |
|
501 | { |
|
502 | 1 | String before = this.mySource.substring(0, this.myIndex); |
503 | 1 | String after = this.mySource.substring(this.myIndex); |
504 | |
|
505 | 1 | return " at character " + this.myIndex + " of " + before + ">>missing value<<" + after; |
506 | } |
|
507 | ||
508 | /** |
|
509 | * Convert <code>%</code><i>hh</i> sequences to single characters, and |
|
510 | * convert plus to space. |
|
511 | * |
|
512 | * @param s |
|
513 | * A string that may contain <code>+</code> <small>(plus)</small> |
|
514 | * and <code>%</code><i>hh</i> sequences. |
|
515 | * @return The unescaped string. |
|
516 | */ |
|
517 | public static String unescape(String s) |
|
518 | { |
|
519 | 0 | int len = s.length(); |
520 | 0 | StringBuffer b = new StringBuffer(); |
521 | 0 | for(int i = 0; i < len; ++i) |
522 | { |
|
523 | 0 | char c = s.charAt(i); |
524 | 0 | if (c == '+') |
525 | { |
|
526 | 0 | c = ' '; |
527 | } |
|
528 | 0 | else if (c == '%' && i + 2 < len) |
529 | { |
|
530 | 0 | int d = dehexchar(s.charAt(i + 1)); |
531 | 0 | int e = dehexchar(s.charAt(i + 2)); |
532 | 0 | if (d >= 0 && e >= 0) |
533 | { |
|
534 | 0 | c = (char) (d * 16 + e); |
535 | 0 | i += 2; |
536 | } |
|
537 | } |
|
538 | 0 | b.append(c); |
539 | } |
|
540 | 0 | return b.toString(); |
541 | } |
|
542 | } |