2 Copyright (c) 2008, Adobe Systems Incorporated
\r
5 Redistribution and use in source and binary forms, with or without
\r
6 modification, are permitted provided that the following conditions are
\r
9 * Redistributions of source code must retain the above copyright notice,
\r
10 this list of conditions and the following disclaimer.
\r
12 * Redistributions in binary form must reproduce the above copyright
\r
13 notice, this list of conditions and the following disclaimer in the
\r
14 documentation and/or other materials provided with the distribution.
\r
16 * Neither the name of Adobe Systems Incorporated nor the names of its
\r
17 contributors may be used to endorse or promote products derived from
\r
18 this software without specific prior written permission.
\r
20 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
\r
21 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
\r
22 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
23 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
\r
24 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
\r
25 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
\r
26 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
\r
27 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
\r
28 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
\r
29 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
\r
30 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\r
33 package com.adobe.serialization.json {
\r
35 public class JSONTokenizer {
\r
37 /** The object that will get parsed from the JSON string */
\r
38 private var obj:Object;
\r
40 /** The JSON string to be parsed */
\r
41 private var jsonString:String;
\r
43 /** The current parsing location in the JSON string */
\r
44 private var loc:int;
\r
46 /** The current character in the JSON string during parsing */
\r
47 private var ch:String;
\r
50 * Constructs a new JSONDecoder to parse a JSON string
\r
51 * into a native object.
\r
53 * @param s The JSON string to be converted
\r
54 * into a native object
\r
56 public function JSONTokenizer( s:String ) {
\r
60 // prime the pump by getting the first character
\r
65 * Gets the next token in the input sting and advances
\r
66 * the character to the next character after the token
\r
68 public function getNextToken():JSONToken {
\r
69 var token:JSONToken = new JSONToken();
\r
71 // skip any whitespace / comments since the last
\r
75 // examine the new character and see what we have...
\r
79 token.type = JSONTokenType.LEFT_BRACE;
\r
85 token.type = JSONTokenType.RIGHT_BRACE;
\r
91 token.type = JSONTokenType.LEFT_BRACKET;
\r
97 token.type = JSONTokenType.RIGHT_BRACKET;
\r
103 token.type = JSONTokenType.COMMA;
\r
109 token.type = JSONTokenType.COLON;
\r
114 case 't': // attempt to read true
\r
115 var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
\r
117 if ( possibleTrue == "true" ) {
\r
118 token.type = JSONTokenType.TRUE;
\r
119 token.value = true;
\r
122 parseError( "Expecting 'true' but found " + possibleTrue );
\r
127 case 'f': // attempt to read false
\r
128 var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
\r
130 if ( possibleFalse == "false" ) {
\r
131 token.type = JSONTokenType.FALSE;
\r
132 token.value = false;
\r
135 parseError( "Expecting 'false' but found " + possibleFalse );
\r
140 case 'n': // attempt to read null
\r
142 var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
\r
144 if ( possibleNull == "null" ) {
\r
145 token.type = JSONTokenType.NULL;
\r
146 token.value = null;
\r
149 parseError( "Expecting 'null' but found " + possibleNull );
\r
154 case '"': // the start of a string
\r
155 token = readString();
\r
159 // see if we can read a number
\r
160 if ( isDigit( ch ) || ch == '-' ) {
\r
161 token = readNumber();
\r
162 } else if ( ch == '' ) {
\r
163 // check for reading past the end of the string
\r
166 // not sure what was in the input string - it's not
\r
167 // anything we expected
\r
168 parseError( "Unexpected " + ch + " encountered" );
\r
176 * Attempts to read a string from the input string. Places
\r
177 * the character location at the first character after the
\r
178 * string. It is assumed that ch is " before this method is called.
\r
180 * @return the JSONToken with the string value if a string could
\r
181 * be read. Throws an error otherwise.
\r
183 private function readString():JSONToken {
\r
184 // the token for the string we'll try to read
\r
185 var token:JSONToken = new JSONToken();
\r
186 token.type = JSONTokenType.STRING;
\r
188 // the string to store the string we'll try to read
\r
189 var string:String = "";
\r
191 // advance past the first "
\r
194 while ( ch != '"' && ch != '' ) {
\r
196 // unescape the escape sequences in the string
\r
197 if ( ch == '\\' ) {
\r
199 // get the next character so we know what
\r
205 case '"': // quotation mark
\r
209 case '/': // solidus
\r
213 case '\\': // reverse solidus
\r
221 case 'f': // form feed
\r
225 case 'n': // newline
\r
229 case 'r': // carriage return
\r
233 case 't': // horizontal tab
\r
238 // convert a unicode escape sequence
\r
239 // to it's character value - expecting
\r
242 // save the characters as a string we'll convert to an int
\r
243 var hexValue:String = "";
\r
245 // try to find 4 hex characters
\r
246 for ( var i:int = 0; i < 4; i++ ) {
\r
247 // get the next character and determine
\r
248 // if it's a valid hex digit or not
\r
249 if ( !isHexDigit( nextChar() ) ) {
\r
250 parseError( " Excepted a hex digit, but found: " + ch );
\r
252 // valid, add it to the value
\r
256 // convert hexValue to an integer, and use that
\r
257 // integrer value to create a character to add
\r
259 string += String.fromCharCode( parseInt( hexValue, 16 ) );
\r
264 // couldn't unescape the sequence, so just
\r
266 string += '\\' + ch;
\r
271 // didn't have to unescape, so add the character to the string
\r
276 // move to the next character
\r
281 // we read past the end of the string without closing it, which
\r
282 // is a parse error
\r
284 parseError( "Unterminated string literal" );
\r
287 // move past the closing " in the input string
\r
290 // attach to the string to the token so we can return it
\r
291 token.value = string;
\r
297 * Attempts to read a number from the input string. Places
\r
298 * the character location at the first character after the
\r
301 * @return The JSONToken with the number value if a number could
\r
302 * be read. Throws an error otherwise.
\r
304 private function readNumber():JSONToken {
\r
305 // the token for the number we'll try to read
\r
306 var token:JSONToken = new JSONToken();
\r
307 token.type = JSONTokenType.NUMBER;
\r
309 // the string to accumulate the number characters
\r
310 // into that we'll convert to a number at the end
\r
311 var input:String = "";
\r
313 // check for a negative number
\r
319 // the number must start with a digit
\r
320 if ( !isDigit( ch ) )
\r
322 parseError( "Expecting a digit" );
\r
325 // 0 can only be the first digit if it
\r
326 // is followed by a decimal point
\r
332 // make sure no other digits come after 0
\r
333 if ( isDigit( ch ) )
\r
335 parseError( "A digit cannot immediately follow 0" );
\r
337 // Commented out - this should only be available when "strict" is false
\r
338 // // unless we have 0x which starts a hex number\
\r
339 // else if ( ch == 'x' )
\r
341 // // include the x in the input
\r
345 // // need at least one hex digit after 0x to
\r
347 // if ( isHexDigit( ch ) )
\r
354 // parseError( "Number in hex format require at least one hex digit after \"0x\"" );
\r
357 // // consume all of the hex values
\r
358 // while ( isHexDigit( ch ) )
\r
367 // read numbers while we can
\r
368 while ( isDigit( ch ) ) {
\r
374 // check for a decimal value
\r
379 // after the decimal there has to be a digit
\r
380 if ( !isDigit( ch ) )
\r
382 parseError( "Expecting a digit" );
\r
385 // read more numbers to get the decimal value
\r
386 while ( isDigit( ch ) ) {
\r
392 // check for scientific notation
\r
393 if ( ch == 'e' || ch == 'E' )
\r
398 if ( ch == '+' || ch == '-' )
\r
404 // require at least one number for the exponent
\r
406 if ( !isDigit( ch ) )
\r
408 parseError( "Scientific notation number needs exponent value" );
\r
411 // read in the exponent
\r
412 while ( isDigit( ch ) )
\r
419 // convert the string to a number value
\r
420 var num:Number = Number( input );
\r
422 if ( isFinite( num ) && !isNaN( num ) ) {
\r
426 parseError( "Number " + num + " is not valid!" );
\r
432 * Reads the next character in the input
\r
433 * string and advances the character location.
\r
435 * @return The next character in the input string, or
\r
436 * null if we've read past the end.
\r
438 private function nextChar():String {
\r
439 return ch = jsonString.charAt( loc++ );
\r
443 * Advances the character location past any
\r
444 * sort of white space and comments
\r
446 private function skipIgnored():void
\r
448 var originalLoc:int;
\r
450 // keep trying to skip whitespace and comments as long
\r
451 // as we keep advancing past the original location
\r
458 while ( originalLoc != loc );
\r
462 * Skips comments in the input string, either
\r
463 * single-line or multi-line. Advances the character
\r
464 * to the first position after the end of the comment.
\r
466 private function skipComments():void {
\r
468 // Advance past the first / to find out what type of comment
\r
471 case '/': // single-line comment, read through end of line
\r
473 // Loop over the characters until we find
\r
474 // a newline or until there's no more characters left
\r
477 } while ( ch != '\n' && ch != '' )
\r
479 // move past the \n
\r
484 case '*': // multi-line comment, read until closing */
\r
486 // move past the opening *
\r
489 // try to find a trailing */
\r
492 // check to see if we have a closing /
\r
495 // move past the end of the closing */
\r
500 // move along, looking if the next character is a *
\r
504 // when we're here we've read past the end of
\r
505 // the string without finding a closing */, so error
\r
507 parseError( "Multi-line comment not closed" );
\r
513 // Can't match a comment after a /, so it's a parsing error
\r
515 parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );
\r
523 * Skip any whitespace in the input string and advances
\r
524 * the character to the first character after any possible
\r
527 private function skipWhite():void {
\r
529 // As long as there are spaces in the input
\r
530 // stream, advance the current location pointer
\r
532 while ( isWhiteSpace( ch ) ) {
\r
539 * Determines if a character is whitespace or not.
\r
541 * @return True if the character passed in is a whitespace
\r
544 private function isWhiteSpace( ch:String ):Boolean {
\r
545 return ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' );
\r
549 * Determines if a character is a digit [0-9].
\r
551 * @return True if the character passed in is a digit
\r
553 private function isDigit( ch:String ):Boolean {
\r
554 return ( ch >= '0' && ch <= '9' );
\r
558 * Determines if a character is a digit [0-9].
\r
560 * @return True if the character passed in is a digit
\r
562 private function isHexDigit( ch:String ):Boolean {
\r
563 // get the uppercase value of ch so we only have
\r
564 // to compare the value between 'A' and 'F'
\r
565 var uc:String = ch.toUpperCase();
\r
567 // a hex digit is a digit of A-F, inclusive ( using
\r
568 // our uppercase constraint )
\r
569 return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) );
\r
573 * Raises a parsing error with a specified message, tacking
\r
574 * on the error location and the original string.
\r
576 * @param message The message indicating why the error occurred
\r
578 public function parseError( message:String ):void {
\r
579 throw new JSONParseError( message, loc, jsonString );
\r