colonymech / docs / www / colonyscout / internal / includes / uploadify / com / adobe / serialization / json / JSONTokenizer.as @ f59acf11
History | View | Annotate | Download (14.6 KB)
1 | f59acf11 | Dan Shope | /* |
---|---|---|---|
2 | Copyright (c) 2008, Adobe Systems Incorporated |
||
3 | All rights reserved. |
||
4 | |||
5 | Redistribution and use in source and binary forms, with or without |
||
6 | modification, are permitted provided that the following conditions are |
||
7 | met: |
||
8 | |||
9 | * Redistributions of source code must retain the above copyright notice, |
||
10 | this list of conditions and the following disclaimer. |
||
11 | |||
12 | * Redistributions in binary form must reproduce the above copyright |
||
13 | notice, this list of conditions and the following disclaimer in the |
||
14 | documentation and/or other materials provided with the distribution. |
||
15 | |||
16 | * Neither the name of Adobe Systems Incorporated nor the names of its |
||
17 | contributors may be used to endorse or promote products derived from |
||
18 | this software without specific prior written permission. |
||
19 | |||
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
||
21 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
||
22 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||
23 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||
24 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||
25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||
26 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||
27 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||
28 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||
29 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||
30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||
31 | */ |
||
32 | |||
33 | package com.adobe.serialization.json { |
||
34 | |||
35 | public class JSONTokenizer { |
||
36 | |||
37 | /** The object that will get parsed from the JSON string */ |
||
38 | private var obj:Object; |
||
39 | |||
40 | /** The JSON string to be parsed */ |
||
41 | private var jsonString:String; |
||
42 | |||
43 | /** The current parsing location in the JSON string */ |
||
44 | private var loc:int; |
||
45 | |||
46 | /** The current character in the JSON string during parsing */ |
||
47 | private var ch:String; |
||
48 | |||
49 | /** |
||
50 | * Constructs a new JSONDecoder to parse a JSON string |
||
51 | * into a native object. |
||
52 | * |
||
53 | * @param s The JSON string to be converted |
||
54 | * into a native object |
||
55 | */ |
||
56 | public function JSONTokenizer( s:String ) { |
||
57 | jsonString = s; |
||
58 | loc = 0; |
||
59 | |||
60 | // prime the pump by getting the first character |
||
61 | nextChar(); |
||
62 | } |
||
63 | |||
64 | /** |
||
65 | * Gets the next token in the input sting and advances |
||
66 | * the character to the next character after the token |
||
67 | */ |
||
68 | public function getNextToken():JSONToken { |
||
69 | var token:JSONToken = new JSONToken(); |
||
70 | |||
71 | // skip any whitespace / comments since the last |
||
72 | // token was read |
||
73 | skipIgnored(); |
||
74 | |||
75 | // examine the new character and see what we have... |
||
76 | switch ( ch ) { |
||
77 | |||
78 | case '{': |
||
79 | token.type = JSONTokenType.LEFT_BRACE; |
||
80 | token.value = '{'; |
||
81 | nextChar(); |
||
82 | break |
||
83 | |||
84 | case '}': |
||
85 | token.type = JSONTokenType.RIGHT_BRACE; |
||
86 | token.value = '}'; |
||
87 | nextChar(); |
||
88 | break |
||
89 | |||
90 | case '[': |
||
91 | token.type = JSONTokenType.LEFT_BRACKET; |
||
92 | token.value = '['; |
||
93 | nextChar(); |
||
94 | break |
||
95 | |||
96 | case ']': |
||
97 | token.type = JSONTokenType.RIGHT_BRACKET; |
||
98 | token.value = ']'; |
||
99 | nextChar(); |
||
100 | break |
||
101 | |||
102 | case ',': |
||
103 | token.type = JSONTokenType.COMMA; |
||
104 | token.value = ','; |
||
105 | nextChar(); |
||
106 | break |
||
107 | |||
108 | case ':': |
||
109 | token.type = JSONTokenType.COLON; |
||
110 | token.value = ':'; |
||
111 | nextChar(); |
||
112 | break; |
||
113 | |||
114 | case 't': // attempt to read true |
||
115 | var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar(); |
||
116 | |||
117 | if ( possibleTrue == "true" ) { |
||
118 | token.type = JSONTokenType.TRUE; |
||
119 | token.value = true; |
||
120 | nextChar(); |
||
121 | } else { |
||
122 | parseError( "Expecting 'true' but found " + possibleTrue ); |
||
123 | } |
||
124 | |||
125 | break; |
||
126 | |||
127 | case 'f': // attempt to read false |
||
128 | var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar(); |
||
129 | |||
130 | if ( possibleFalse == "false" ) { |
||
131 | token.type = JSONTokenType.FALSE; |
||
132 | token.value = false; |
||
133 | nextChar(); |
||
134 | } else { |
||
135 | parseError( "Expecting 'false' but found " + possibleFalse ); |
||
136 | } |
||
137 | |||
138 | break; |
||
139 | |||
140 | case 'n': // attempt to read null |
||
141 | |||
142 | var possibleNull:String = "n" + nextChar() + nextChar() + nextChar(); |
||
143 | |||
144 | if ( possibleNull == "null" ) { |
||
145 | token.type = JSONTokenType.NULL; |
||
146 | token.value = null; |
||
147 | nextChar(); |
||
148 | } else { |
||
149 | parseError( "Expecting 'null' but found " + possibleNull ); |
||
150 | } |
||
151 | |||
152 | break; |
||
153 | |||
154 | case '"': // the start of a string |
||
155 | token = readString(); |
||
156 | break; |
||
157 | |||
158 | default: |
||
159 | // see if we can read a number |
||
160 | if ( isDigit( ch ) || ch == '-' ) { |
||
161 | token = readNumber(); |
||
162 | } else if ( ch == '' ) { |
||
163 | // check for reading past the end of the string |
||
164 | return null; |
||
165 | } else { |
||
166 | // not sure what was in the input string - it's not |
||
167 | // anything we expected |
||
168 | parseError( "Unexpected " + ch + " encountered" ); |
||
169 | } |
||
170 | } |
||
171 | |||
172 | return token; |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * Attempts to read a string from the input string. Places |
||
177 | * the character location at the first character after the |
||
178 | * string. It is assumed that ch is " before this method is called. |
||
179 | * |
||
180 | * @return the JSONToken with the string value if a string could |
||
181 | * be read. Throws an error otherwise. |
||
182 | */ |
||
183 | private function readString():JSONToken { |
||
184 | // the token for the string we'll try to read |
||
185 | var token:JSONToken = new JSONToken(); |
||
186 | token.type = JSONTokenType.STRING; |
||
187 | |||
188 | // the string to store the string we'll try to read |
||
189 | var string:String = ""; |
||
190 | |||
191 | // advance past the first " |
||
192 | nextChar(); |
||
193 | |||
194 | while ( ch != '"' && ch != '' ) { |
||
195 | |||
196 | // unescape the escape sequences in the string |
||
197 | if ( ch == '\\' ) { |
||
198 | |||
199 | // get the next character so we know what |
||
200 | // to unescape |
||
201 | nextChar(); |
||
202 | |||
203 | switch ( ch ) { |
||
204 | |||
205 | case '"': // quotation mark |
||
206 | string += '"'; |
||
207 | break; |
||
208 | |||
209 | case '/': // solidus |
||
210 | string += "/"; |
||
211 | break; |
||
212 | |||
213 | case '\\': // reverse solidus |
||
214 | string += '\\'; |
||
215 | break; |
||
216 | |||
217 | case 'b': // bell |
||
218 | string += '\b'; |
||
219 | break; |
||
220 | |||
221 | case 'f': // form feed |
||
222 | string += '\f'; |
||
223 | break; |
||
224 | |||
225 | case 'n': // newline |
||
226 | string += '\n'; |
||
227 | break; |
||
228 | |||
229 | case 'r': // carriage return |
||
230 | string += '\r'; |
||
231 | break; |
||
232 | |||
233 | case 't': // horizontal tab |
||
234 | string += '\t' |
||
235 | break; |
||
236 | |||
237 | case 'u': |
||
238 | // convert a unicode escape sequence |
||
239 | // to it's character value - expecting |
||
240 | // 4 hex digits |
||
241 | |||
242 | // save the characters as a string we'll convert to an int |
||
243 | var hexValue:String = ""; |
||
244 | |||
245 | // try to find 4 hex characters |
||
246 | for ( var i:int = 0; i < 4; i++ ) { |
||
247 | // get the next character and determine |
||
248 | // if it's a valid hex digit or not |
||
249 | if ( !isHexDigit( nextChar() ) ) { |
||
250 | parseError( " Excepted a hex digit, but found: " + ch ); |
||
251 | } |
||
252 | // valid, add it to the value |
||
253 | hexValue += ch; |
||
254 | } |
||
255 | |||
256 | // convert hexValue to an integer, and use that |
||
257 | // integrer value to create a character to add |
||
258 | // to our string. |
||
259 | string += String.fromCharCode( parseInt( hexValue, 16 ) ); |
||
260 | |||
261 | break; |
||
262 | |||
263 | default: |
||
264 | // couldn't unescape the sequence, so just |
||
265 | // pass it through |
||
266 | string += '\\' + ch; |
||
267 | |||
268 | } |
||
269 | |||
270 | } else { |
||
271 | // didn't have to unescape, so add the character to the string |
||
272 | string += ch; |
||
273 | |||
274 | } |
||
275 | |||
276 | // move to the next character |
||
277 | nextChar(); |
||
278 | |||
279 | } |
||
280 | |||
281 | // we read past the end of the string without closing it, which |
||
282 | // is a parse error |
||
283 | if ( ch == '' ) { |
||
284 | parseError( "Unterminated string literal" ); |
||
285 | } |
||
286 | |||
287 | // move past the closing " in the input string |
||
288 | nextChar(); |
||
289 | |||
290 | // attach to the string to the token so we can return it |
||
291 | token.value = string; |
||
292 | |||
293 | return token; |
||
294 | } |
||
295 | |||
296 | /** |
||
297 | * Attempts to read a number from the input string. Places |
||
298 | * the character location at the first character after the |
||
299 | * number. |
||
300 | * |
||
301 | * @return The JSONToken with the number value if a number could |
||
302 | * be read. Throws an error otherwise. |
||
303 | */ |
||
304 | private function readNumber():JSONToken { |
||
305 | // the token for the number we'll try to read |
||
306 | var token:JSONToken = new JSONToken(); |
||
307 | token.type = JSONTokenType.NUMBER; |
||
308 | |||
309 | // the string to accumulate the number characters |
||
310 | // into that we'll convert to a number at the end |
||
311 | var input:String = ""; |
||
312 | |||
313 | // check for a negative number |
||
314 | if ( ch == '-' ) { |
||
315 | input += '-'; |
||
316 | nextChar(); |
||
317 | } |
||
318 | |||
319 | // the number must start with a digit |
||
320 | if ( !isDigit( ch ) ) |
||
321 | { |
||
322 | parseError( "Expecting a digit" ); |
||
323 | } |
||
324 | |||
325 | // 0 can only be the first digit if it |
||
326 | // is followed by a decimal point |
||
327 | if ( ch == '0' ) |
||
328 | { |
||
329 | input += ch; |
||
330 | nextChar(); |
||
331 | |||
332 | // make sure no other digits come after 0 |
||
333 | if ( isDigit( ch ) ) |
||
334 | { |
||
335 | parseError( "A digit cannot immediately follow 0" ); |
||
336 | } |
||
337 | // Commented out - this should only be available when "strict" is false |
||
338 | // // unless we have 0x which starts a hex number\ |
||
339 | // else if ( ch == 'x' ) |
||
340 | // { |
||
341 | // // include the x in the input |
||
342 | // input += ch; |
||
343 | // nextChar(); |
||
344 | // |
||
345 | // // need at least one hex digit after 0x to |
||
346 | // // be valid |
||
347 | // if ( isHexDigit( ch ) ) |
||
348 | // { |
||
349 | // input += ch; |
||
350 | // nextChar(); |
||
351 | // } |
||
352 | // else |
||
353 | // { |
||
354 | // parseError( "Number in hex format require at least one hex digit after \"0x\"" ); |
||
355 | // } |
||
356 | // |
||
357 | // // consume all of the hex values |
||
358 | // while ( isHexDigit( ch ) ) |
||
359 | // { |
||
360 | // input += ch; |
||
361 | // nextChar(); |
||
362 | // } |
||
363 | // } |
||
364 | } |
||
365 | else |
||
366 | { |
||
367 | // read numbers while we can |
||
368 | while ( isDigit( ch ) ) { |
||
369 | input += ch; |
||
370 | nextChar(); |
||
371 | } |
||
372 | } |
||
373 | |||
374 | // check for a decimal value |
||
375 | if ( ch == '.' ) { |
||
376 | input += '.'; |
||
377 | nextChar(); |
||
378 | |||
379 | // after the decimal there has to be a digit |
||
380 | if ( !isDigit( ch ) ) |
||
381 | { |
||
382 | parseError( "Expecting a digit" ); |
||
383 | } |
||
384 | |||
385 | // read more numbers to get the decimal value |
||
386 | while ( isDigit( ch ) ) { |
||
387 | input += ch; |
||
388 | nextChar(); |
||
389 | } |
||
390 | } |
||
391 | |||
392 | // check for scientific notation |
||
393 | if ( ch == 'e' || ch == 'E' ) |
||
394 | { |
||
395 | input += "e" |
||
396 | nextChar(); |
||
397 | // check for sign |
||
398 | if ( ch == '+' || ch == '-' ) |
||
399 | { |
||
400 | input += ch; |
||
401 | nextChar(); |
||
402 | } |
||
403 | |||
404 | // require at least one number for the exponent |
||
405 | // in this case |
||
406 | if ( !isDigit( ch ) ) |
||
407 | { |
||
408 | parseError( "Scientific notation number needs exponent value" ); |
||
409 | } |
||
410 | |||
411 | // read in the exponent |
||
412 | while ( isDigit( ch ) ) |
||
413 | { |
||
414 | input += ch; |
||
415 | nextChar(); |
||
416 | } |
||
417 | } |
||
418 | |||
419 | // convert the string to a number value |
||
420 | var num:Number = Number( input ); |
||
421 | |||
422 | if ( isFinite( num ) && !isNaN( num ) ) { |
||
423 | token.value = num; |
||
424 | return token; |
||
425 | } else { |
||
426 | parseError( "Number " + num + " is not valid!" ); |
||
427 | } |
||
428 | return null; |
||
429 | } |
||
430 | |||
431 | /** |
||
432 | * Reads the next character in the input |
||
433 | * string and advances the character location. |
||
434 | * |
||
435 | * @return The next character in the input string, or |
||
436 | * null if we've read past the end. |
||
437 | */ |
||
438 | private function nextChar():String { |
||
439 | return ch = jsonString.charAt( loc++ ); |
||
440 | } |
||
441 | |||
442 | /** |
||
443 | * Advances the character location past any |
||
444 | * sort of white space and comments |
||
445 | */ |
||
446 | private function skipIgnored():void |
||
447 | { |
||
448 | var originalLoc:int; |
||
449 | |||
450 | // keep trying to skip whitespace and comments as long |
||
451 | // as we keep advancing past the original location |
||
452 | do |
||
453 | { |
||
454 | originalLoc = loc; |
||
455 | skipWhite(); |
||
456 | skipComments(); |
||
457 | } |
||
458 | while ( originalLoc != loc ); |
||
459 | } |
||
460 | |||
461 | /** |
||
462 | * Skips comments in the input string, either |
||
463 | * single-line or multi-line. Advances the character |
||
464 | * to the first position after the end of the comment. |
||
465 | */ |
||
466 | private function skipComments():void { |
||
467 | if ( ch == '/' ) { |
||
468 | // Advance past the first / to find out what type of comment |
||
469 | nextChar(); |
||
470 | switch ( ch ) { |
||
471 | case '/': // single-line comment, read through end of line |
||
472 | |||
473 | // Loop over the characters until we find |
||
474 | // a newline or until there's no more characters left |
||
475 | do { |
||
476 | nextChar(); |
||
477 | } while ( ch != '\n' && ch != '' ) |
||
478 | |||
479 | // move past the \n |
||
480 | nextChar(); |
||
481 | |||
482 | break; |
||
483 | |||
484 | case '*': // multi-line comment, read until closing */ |
||
485 | |||
486 | // move past the opening * |
||
487 | nextChar(); |
||
488 | |||
489 | // try to find a trailing */ |
||
490 | while ( true ) { |
||
491 | if ( ch == '*' ) { |
||
492 | // check to see if we have a closing / |
||
493 | nextChar(); |
||
494 | if ( ch == '/') { |
||
495 | // move past the end of the closing */ |
||
496 | nextChar(); |
||
497 | break; |
||
498 | } |
||
499 | } else { |
||
500 | // move along, looking if the next character is a * |
||
501 | nextChar(); |
||
502 | } |
||
503 | |||
504 | // when we're here we've read past the end of |
||
505 | // the string without finding a closing */, so error |
||
506 | if ( ch == '' ) { |
||
507 | parseError( "Multi-line comment not closed" ); |
||
508 | } |
||
509 | } |
||
510 | |||
511 | break; |
||
512 | |||
513 | // Can't match a comment after a /, so it's a parsing error |
||
514 | default: |
||
515 | parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" ); |
||
516 | } |
||
517 | } |
||
518 | |||
519 | } |
||
520 | |||
521 | |||
522 | /** |
||
523 | * Skip any whitespace in the input string and advances |
||
524 | * the character to the first character after any possible |
||
525 | * whitespace. |
||
526 | */ |
||
527 | private function skipWhite():void { |
||
528 | |||
529 | // As long as there are spaces in the input |
||
530 | // stream, advance the current location pointer |
||
531 | // past them |
||
532 | while ( isWhiteSpace( ch ) ) { |
||
533 | nextChar(); |
||
534 | } |
||
535 | |||
536 | } |
||
537 | |||
538 | /** |
||
539 | * Determines if a character is whitespace or not. |
||
540 | * |
||
541 | * @return True if the character passed in is a whitespace |
||
542 | * character |
||
543 | */ |
||
544 | private function isWhiteSpace( ch:String ):Boolean { |
||
545 | return ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' ); |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Determines if a character is a digit [0-9]. |
||
550 | * |
||
551 | * @return True if the character passed in is a digit |
||
552 | */ |
||
553 | private function isDigit( ch:String ):Boolean { |
||
554 | return ( ch >= '0' && ch <= '9' ); |
||
555 | } |
||
556 | |||
557 | /** |
||
558 | * Determines if a character is a digit [0-9]. |
||
559 | * |
||
560 | * @return True if the character passed in is a digit |
||
561 | */ |
||
562 | private function isHexDigit( ch:String ):Boolean { |
||
563 | // get the uppercase value of ch so we only have |
||
564 | // to compare the value between 'A' and 'F' |
||
565 | var uc:String = ch.toUpperCase(); |
||
566 | |||
567 | // a hex digit is a digit of A-F, inclusive ( using |
||
568 | // our uppercase constraint ) |
||
569 | return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) ); |
||
570 | } |
||
571 | |||
572 | /** |
||
573 | * Raises a parsing error with a specified message, tacking |
||
574 | * on the error location and the original string. |
||
575 | * |
||
576 | * @param message The message indicating why the error occurred |
||
577 | */ |
||
578 | public function parseError( message:String ):void { |
||
579 | throw new JSONParseError( message, loc, jsonString ); |
||
580 | } |
||
581 | } |
||
582 | |||
583 | } |