Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
JSONTokener |
|
| 7.352941176470588;7.353 |
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 | 0 | { |
61 | 0 | this.myIndex = 0; |
62 | 0 | this.mySource = s; |
63 | 0 | } |
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 | 0 | if (this.myIndex > 0) |
73 | { | |
74 | 0 | this.myIndex -= 1; |
75 | } | |
76 | 0 | } |
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 | 0 | 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 | 0 | if (more()) |
113 | { | |
114 | 0 | char c = this.mySource.charAt(this.myIndex); |
115 | 0 | this.myIndex += 1; |
116 | 0 | return c; |
117 | } | |
118 | 0 | 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 | 0 | char c = next(); |
172 | 0 | 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 | 0 | else if (c == '#') |
203 | { | |
204 | do | |
205 | { | |
206 | 0 | c = next(); |
207 | 0 | } while(c != '\n' && c != '\r' && c != 0); |
208 | } | |
209 | 0 | 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 | 0 | char c = nextClean(); |
341 | String s; | |
342 | ||
343 | 0 | 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 | 0 | StringBuffer sb = new StringBuffer(); |
364 | 0 | char b = c; |
365 | 0 | while(c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) |
366 | { | |
367 | 0 | sb.append(c); |
368 | 0 | c = next(); |
369 | } | |
370 | 0 | back(); |
371 | ||
372 | /* | |
373 | * If it is true, false, or null, return the proper value. | |
374 | */ | |
375 | ||
376 | 0 | s = sb.toString().trim(); |
377 | 0 | if (s.equals("")) { throw syntaxError("Missing value."); } |
378 | 0 | if (s.equalsIgnoreCase("true")) { return Boolean.TRUE; } |
379 | 0 | if (s.equalsIgnoreCase("false")) { return Boolean.FALSE; } |
380 | 0 | 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 | 0 | 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 | 0 | 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 | 0 | 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 | 0 | String before = this.mySource.substring(0, this.myIndex); |
503 | 0 | String after = this.mySource.substring(this.myIndex); |
504 | | |
505 | 0 | 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 | } |