--- /dev/null
+/*\r
+ Copyright (c) 2008, Adobe Systems Incorporated\r
+ All rights reserved.\r
+\r
+ Redistribution and use in source and binary forms, with or without \r
+ modification, are permitted provided that the following conditions are\r
+ met:\r
+\r
+ * Redistributions of source code must retain the above copyright notice, \r
+ this list of conditions and the following disclaimer.\r
+ \r
+ * Redistributions in binary form must reproduce the above copyright\r
+ notice, this list of conditions and the following disclaimer in the \r
+ documentation and/or other materials provided with the distribution.\r
+ \r
+ * Neither the name of Adobe Systems Incorporated nor the names of its \r
+ contributors may be used to endorse or promote products derived from \r
+ this software without specific prior written permission.\r
+\r
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS\r
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\r
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\r
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR \r
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\r
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\r
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\r
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\r
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\r
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+*/\r
+\r
+package com.adobe.serialization.json {\r
+\r
+ public class JSONTokenizer {\r
+ \r
+ /** The object that will get parsed from the JSON string */\r
+ private var obj:Object;\r
+ \r
+ /** The JSON string to be parsed */\r
+ private var jsonString:String;\r
+ \r
+ /** The current parsing location in the JSON string */\r
+ private var loc:int;\r
+ \r
+ /** The current character in the JSON string during parsing */\r
+ private var ch:String;\r
+ \r
+ /**\r
+ * Constructs a new JSONDecoder to parse a JSON string \r
+ * into a native object.\r
+ *\r
+ * @param s The JSON string to be converted\r
+ * into a native object\r
+ */\r
+ public function JSONTokenizer( s:String ) {\r
+ jsonString = s;\r
+ loc = 0;\r
+ \r
+ // prime the pump by getting the first character\r
+ nextChar();\r
+ }\r
+ \r
+ /**\r
+ * Gets the next token in the input sting and advances\r
+ * the character to the next character after the token\r
+ */\r
+ public function getNextToken():JSONToken {\r
+ var token:JSONToken = new JSONToken();\r
+ \r
+ // skip any whitespace / comments since the last \r
+ // token was read\r
+ skipIgnored();\r
+ \r
+ // examine the new character and see what we have...\r
+ switch ( ch ) {\r
+ \r
+ case '{':\r
+ token.type = JSONTokenType.LEFT_BRACE;\r
+ token.value = '{';\r
+ nextChar();\r
+ break\r
+ \r
+ case '}':\r
+ token.type = JSONTokenType.RIGHT_BRACE;\r
+ token.value = '}';\r
+ nextChar();\r
+ break\r
+ \r
+ case '[':\r
+ token.type = JSONTokenType.LEFT_BRACKET;\r
+ token.value = '[';\r
+ nextChar();\r
+ break\r
+ \r
+ case ']':\r
+ token.type = JSONTokenType.RIGHT_BRACKET;\r
+ token.value = ']';\r
+ nextChar();\r
+ break\r
+ \r
+ case ',':\r
+ token.type = JSONTokenType.COMMA;\r
+ token.value = ',';\r
+ nextChar();\r
+ break\r
+ \r
+ case ':':\r
+ token.type = JSONTokenType.COLON;\r
+ token.value = ':';\r
+ nextChar();\r
+ break;\r
+ \r
+ case 't': // attempt to read true\r
+ var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();\r
+ \r
+ if ( possibleTrue == "true" ) {\r
+ token.type = JSONTokenType.TRUE;\r
+ token.value = true;\r
+ nextChar();\r
+ } else {\r
+ parseError( "Expecting 'true' but found " + possibleTrue );\r
+ }\r
+ \r
+ break;\r
+ \r
+ case 'f': // attempt to read false\r
+ var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();\r
+ \r
+ if ( possibleFalse == "false" ) {\r
+ token.type = JSONTokenType.FALSE;\r
+ token.value = false;\r
+ nextChar();\r
+ } else {\r
+ parseError( "Expecting 'false' but found " + possibleFalse );\r
+ }\r
+ \r
+ break;\r
+ \r
+ case 'n': // attempt to read null\r
+ \r
+ var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();\r
+ \r
+ if ( possibleNull == "null" ) {\r
+ token.type = JSONTokenType.NULL;\r
+ token.value = null;\r
+ nextChar();\r
+ } else {\r
+ parseError( "Expecting 'null' but found " + possibleNull );\r
+ }\r
+ \r
+ break;\r
+ \r
+ case '"': // the start of a string\r
+ token = readString();\r
+ break;\r
+ \r
+ default: \r
+ // see if we can read a number\r
+ if ( isDigit( ch ) || ch == '-' ) {\r
+ token = readNumber();\r
+ } else if ( ch == '' ) {\r
+ // check for reading past the end of the string\r
+ return null;\r
+ } else { \r
+ // not sure what was in the input string - it's not\r
+ // anything we expected\r
+ parseError( "Unexpected " + ch + " encountered" );\r
+ }\r
+ }\r
+ \r
+ return token;\r
+ }\r
+ \r
+ /**\r
+ * Attempts to read a string from the input string. Places\r
+ * the character location at the first character after the\r
+ * string. It is assumed that ch is " before this method is called.\r
+ *\r
+ * @return the JSONToken with the string value if a string could\r
+ * be read. Throws an error otherwise.\r
+ */\r
+ private function readString():JSONToken {\r
+ // the token for the string we'll try to read\r
+ var token:JSONToken = new JSONToken();\r
+ token.type = JSONTokenType.STRING;\r
+ \r
+ // the string to store the string we'll try to read\r
+ var string:String = "";\r
+ \r
+ // advance past the first "\r
+ nextChar();\r
+ \r
+ while ( ch != '"' && ch != '' ) {\r
+ \r
+ // unescape the escape sequences in the string\r
+ if ( ch == '\\' ) {\r
+ \r
+ // get the next character so we know what\r
+ // to unescape\r
+ nextChar();\r
+ \r
+ switch ( ch ) {\r
+ \r
+ case '"': // quotation mark\r
+ string += '"';\r
+ break;\r
+ \r
+ case '/': // solidus\r
+ string += "/";\r
+ break;\r
+ \r
+ case '\\': // reverse solidus\r
+ string += '\\';\r
+ break;\r
+ \r
+ case 'b': // bell\r
+ string += '\b';\r
+ break;\r
+ \r
+ case 'f': // form feed\r
+ string += '\f';\r
+ break;\r
+ \r
+ case 'n': // newline\r
+ string += '\n';\r
+ break;\r
+ \r
+ case 'r': // carriage return\r
+ string += '\r';\r
+ break;\r
+ \r
+ case 't': // horizontal tab\r
+ string += '\t'\r
+ break;\r
+ \r
+ case 'u':\r
+ // convert a unicode escape sequence\r
+ // to it's character value - expecting\r
+ // 4 hex digits\r
+ \r
+ // save the characters as a string we'll convert to an int\r
+ var hexValue:String = "";\r
+ \r
+ // try to find 4 hex characters\r
+ for ( var i:int = 0; i < 4; i++ ) {\r
+ // get the next character and determine\r
+ // if it's a valid hex digit or not\r
+ if ( !isHexDigit( nextChar() ) ) {\r
+ parseError( " Excepted a hex digit, but found: " + ch );\r
+ }\r
+ // valid, add it to the value\r
+ hexValue += ch;\r
+ }\r
+ \r
+ // convert hexValue to an integer, and use that\r
+ // integrer value to create a character to add\r
+ // to our string.\r
+ string += String.fromCharCode( parseInt( hexValue, 16 ) );\r
+ \r
+ break;\r
+ \r
+ default:\r
+ // couldn't unescape the sequence, so just\r
+ // pass it through\r
+ string += '\\' + ch;\r
+ \r
+ }\r
+ \r
+ } else {\r
+ // didn't have to unescape, so add the character to the string\r
+ string += ch;\r
+ \r
+ }\r
+ \r
+ // move to the next character\r
+ nextChar();\r
+ \r
+ }\r
+ \r
+ // we read past the end of the string without closing it, which\r
+ // is a parse error\r
+ if ( ch == '' ) {\r
+ parseError( "Unterminated string literal" );\r
+ }\r
+ \r
+ // move past the closing " in the input string\r
+ nextChar();\r
+ \r
+ // attach to the string to the token so we can return it\r
+ token.value = string;\r
+ \r
+ return token;\r
+ }\r
+ \r
+ /**\r
+ * Attempts to read a number from the input string. Places\r
+ * the character location at the first character after the\r
+ * number.\r
+ * \r
+ * @return The JSONToken with the number value if a number could\r
+ * be read. Throws an error otherwise.\r
+ */\r
+ private function readNumber():JSONToken {\r
+ // the token for the number we'll try to read\r
+ var token:JSONToken = new JSONToken();\r
+ token.type = JSONTokenType.NUMBER;\r
+ \r
+ // the string to accumulate the number characters\r
+ // into that we'll convert to a number at the end\r
+ var input:String = "";\r
+ \r
+ // check for a negative number\r
+ if ( ch == '-' ) {\r
+ input += '-';\r
+ nextChar();\r
+ }\r
+ \r
+ // the number must start with a digit\r
+ if ( !isDigit( ch ) )\r
+ {\r
+ parseError( "Expecting a digit" );\r
+ }\r
+ \r
+ // 0 can only be the first digit if it\r
+ // is followed by a decimal point\r
+ if ( ch == '0' )\r
+ {\r
+ input += ch;\r
+ nextChar();\r
+ \r
+ // make sure no other digits come after 0\r
+ if ( isDigit( ch ) )\r
+ {\r
+ parseError( "A digit cannot immediately follow 0" );\r
+ }\r
+// Commented out - this should only be available when "strict" is false\r
+// // unless we have 0x which starts a hex number\\r
+// else if ( ch == 'x' )\r
+// {\r
+// // include the x in the input\r
+// input += ch;\r
+// nextChar();\r
+// \r
+// // need at least one hex digit after 0x to\r
+// // be valid\r
+// if ( isHexDigit( ch ) )\r
+// {\r
+// input += ch;\r
+// nextChar();\r
+// }\r
+// else\r
+// {\r
+// parseError( "Number in hex format require at least one hex digit after \"0x\"" ); \r
+// }\r
+// \r
+// // consume all of the hex values\r
+// while ( isHexDigit( ch ) )\r
+// {\r
+// input += ch;\r
+// nextChar();\r
+// }\r
+// }\r
+ }\r
+ else\r
+ {\r
+ // read numbers while we can\r
+ while ( isDigit( ch ) ) {\r
+ input += ch;\r
+ nextChar();\r
+ }\r
+ }\r
+ \r
+ // check for a decimal value\r
+ if ( ch == '.' ) {\r
+ input += '.';\r
+ nextChar();\r
+ \r
+ // after the decimal there has to be a digit\r
+ if ( !isDigit( ch ) )\r
+ {\r
+ parseError( "Expecting a digit" );\r
+ }\r
+ \r
+ // read more numbers to get the decimal value\r
+ while ( isDigit( ch ) ) {\r
+ input += ch;\r
+ nextChar();\r
+ }\r
+ }\r
+ \r
+ // check for scientific notation\r
+ if ( ch == 'e' || ch == 'E' )\r
+ {\r
+ input += "e"\r
+ nextChar();\r
+ // check for sign\r
+ if ( ch == '+' || ch == '-' )\r
+ {\r
+ input += ch;\r
+ nextChar();\r
+ }\r
+ \r
+ // require at least one number for the exponent\r
+ // in this case\r
+ if ( !isDigit( ch ) )\r
+ {\r
+ parseError( "Scientific notation number needs exponent value" );\r
+ }\r
+ \r
+ // read in the exponent\r
+ while ( isDigit( ch ) )\r
+ {\r
+ input += ch;\r
+ nextChar();\r
+ }\r
+ }\r
+ \r
+ // convert the string to a number value\r
+ var num:Number = Number( input );\r
+ \r
+ if ( isFinite( num ) && !isNaN( num ) ) {\r
+ token.value = num;\r
+ return token;\r
+ } else {\r
+ parseError( "Number " + num + " is not valid!" );\r
+ }\r
+ return null;\r
+ }\r
+\r
+ /**\r
+ * Reads the next character in the input\r
+ * string and advances the character location.\r
+ *\r
+ * @return The next character in the input string, or\r
+ * null if we've read past the end.\r
+ */\r
+ private function nextChar():String {\r
+ return ch = jsonString.charAt( loc++ );\r
+ }\r
+ \r
+ /**\r
+ * Advances the character location past any\r
+ * sort of white space and comments\r
+ */\r
+ private function skipIgnored():void\r
+ {\r
+ var originalLoc:int;\r
+ \r
+ // keep trying to skip whitespace and comments as long\r
+ // as we keep advancing past the original location \r
+ do\r
+ {\r
+ originalLoc = loc;\r
+ skipWhite();\r
+ skipComments();\r
+ }\r
+ while ( originalLoc != loc );\r
+ }\r
+ \r
+ /**\r
+ * Skips comments in the input string, either\r
+ * single-line or multi-line. Advances the character\r
+ * to the first position after the end of the comment.\r
+ */\r
+ private function skipComments():void {\r
+ if ( ch == '/' ) {\r
+ // Advance past the first / to find out what type of comment\r
+ nextChar();\r
+ switch ( ch ) {\r
+ case '/': // single-line comment, read through end of line\r
+ \r
+ // Loop over the characters until we find\r
+ // a newline or until there's no more characters left\r
+ do {\r
+ nextChar();\r
+ } while ( ch != '\n' && ch != '' )\r
+ \r
+ // move past the \n\r
+ nextChar();\r
+ \r
+ break;\r
+ \r
+ case '*': // multi-line comment, read until closing */\r
+\r
+ // move past the opening *\r
+ nextChar();\r
+ \r
+ // try to find a trailing */\r
+ while ( true ) {\r
+ if ( ch == '*' ) {\r
+ // check to see if we have a closing /\r
+ nextChar();\r
+ if ( ch == '/') {\r
+ // move past the end of the closing */\r
+ nextChar();\r
+ break;\r
+ }\r
+ } else {\r
+ // move along, looking if the next character is a *\r
+ nextChar();\r
+ }\r
+ \r
+ // when we're here we've read past the end of \r
+ // the string without finding a closing */, so error\r
+ if ( ch == '' ) {\r
+ parseError( "Multi-line comment not closed" );\r
+ }\r
+ }\r
+\r
+ break;\r
+ \r
+ // Can't match a comment after a /, so it's a parsing error\r
+ default:\r
+ parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );\r
+ }\r
+ }\r
+ \r
+ }\r
+ \r
+ \r
+ /**\r
+ * Skip any whitespace in the input string and advances\r
+ * the character to the first character after any possible\r
+ * whitespace.\r
+ */\r
+ private function skipWhite():void {\r
+ \r
+ // As long as there are spaces in the input \r
+ // stream, advance the current location pointer\r
+ // past them\r
+ while ( isWhiteSpace( ch ) ) {\r
+ nextChar();\r
+ }\r
+ \r
+ }\r
+ \r
+ /**\r
+ * Determines if a character is whitespace or not.\r
+ *\r
+ * @return True if the character passed in is a whitespace\r
+ * character\r
+ */\r
+ private function isWhiteSpace( ch:String ):Boolean {\r
+ return ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' );\r
+ }\r
+ \r
+ /**\r
+ * Determines if a character is a digit [0-9].\r
+ *\r
+ * @return True if the character passed in is a digit\r
+ */\r
+ private function isDigit( ch:String ):Boolean {\r
+ return ( ch >= '0' && ch <= '9' );\r
+ }\r
+ \r
+ /**\r
+ * Determines if a character is a digit [0-9].\r
+ *\r
+ * @return True if the character passed in is a digit\r
+ */\r
+ private function isHexDigit( ch:String ):Boolean {\r
+ // get the uppercase value of ch so we only have\r
+ // to compare the value between 'A' and 'F'\r
+ var uc:String = ch.toUpperCase();\r
+ \r
+ // a hex digit is a digit of A-F, inclusive ( using\r
+ // our uppercase constraint )\r
+ return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) );\r
+ }\r
+ \r
+ /**\r
+ * Raises a parsing error with a specified message, tacking\r
+ * on the error location and the original string.\r
+ *\r
+ * @param message The message indicating why the error occurred\r
+ */\r
+ public function parseError( message:String ):void {\r
+ throw new JSONParseError( message, loc, jsonString );\r
+ }\r
+ }\r
+ \r
+}\r