package com.adobe.serialization.json {
public class JSONTokenizer {
/** The object that will get parsed from the JSON string */
private var obj:Object;
/** The JSON string to be parsed */
private var jsonString:String;
/** The current parsing location in the JSON string */
private var loc:int;
/** The current character in the JSON string during parsing */
private var ch:String;
/**
* Constructs a new JSONDecoder to parse a JSON string
* into a native object.
*
* @param s The JSON string to be converted
* into a native object
*/
public function JSONTokenizer( s:String ) {
jsonString = s;
loc = 0;
nextChar();
}
/**
* Gets the next token in the input sting and advances
* the character to the next character after the token
*/
public function getNextToken():JSONToken {
var token:JSONToken = new JSONToken();
skipIgnored();
switch ( ch ) {
case '{':
token.type = JSONTokenType.LEFT_BRACE;
token.value = '{';
nextChar();
break
case '}':
token.type = JSONTokenType.RIGHT_BRACE;
token.value = '}';
nextChar();
break
case '[':
token.type = JSONTokenType.LEFT_BRACKET;
token.value = '[';
nextChar();
break
case ']':
token.type = JSONTokenType.RIGHT_BRACKET;
token.value = ']';
nextChar();
break
case ',':
token.type = JSONTokenType.COMMA;
token.value = ',';
nextChar();
break
case ':':
token.type = JSONTokenType.COLON;
token.value = ':';
nextChar();
break;
case 't': var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
if ( possibleTrue == "true" ) {
token.type = JSONTokenType.TRUE;
token.value = true;
nextChar();
} else {
parseError( "Expecting 'true' but found " + possibleTrue );
}
break;
case 'f': var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
if ( possibleFalse == "false" ) {
token.type = JSONTokenType.FALSE;
token.value = false;
nextChar();
} else {
parseError( "Expecting 'false' but found " + possibleFalse );
}
break;
case 'n':
var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
if ( possibleNull == "null" ) {
token.type = JSONTokenType.NULL;
token.value = null;
nextChar();
} else {
parseError( "Expecting 'null' but found " + possibleNull );
}
break;
case '"': token = readString();
break;
default:
if ( isDigit( ch ) || ch == '-' ) {
token = readNumber();
} else if ( ch == '' ) {
return null;
} else {
parseError( "Unexpected " + ch + " encountered" );
}
}
return token;
}
/**
* Attempts to read a string from the input string. Places
* the character location at the first character after the
* string. It is assumed that ch is " before this method is called.
*
* @return the JSONToken with the string value if a string could
* be read. Throws an error otherwise.
*/
private function readString():JSONToken {
var token:JSONToken = new JSONToken();
token.type = JSONTokenType.STRING;
var string:String = "";
nextChar();
while ( ch != '"' && ch != '' ) {
if ( ch == '\\' ) {
nextChar();
switch ( ch ) {
case '"': string += '"';
break;
case '/': string += "/";
break;
case '\\': string += '\\';
break;
case 'b': string += '\b';
break;
case 'f': string += '\f';
break;
case 'n': string += '\n';
break;
case 'r': string += '\r';
break;
case 't': string += '\t'
break;
case 'u':
var hexValue:String = "";
for ( var i:int = 0; i < 4; i++ ) {
if ( !isHexDigit( nextChar() ) ) {
parseError( " Excepted a hex digit, but found: " + ch );
}
hexValue += ch;
}
string += String.fromCharCode( parseInt( hexValue, 16 ) );
break;
default:
string += '\\' + ch;
}
} else {
string += ch;
}
nextChar();
}
if ( ch == '' ) {
parseError( "Unterminated string literal" );
}
nextChar();
token.value = string;
return token;
}
/**
* Attempts to read a number from the input string. Places
* the character location at the first character after the
* number.
*
* @return The JSONToken with the number value if a number could
* be read. Throws an error otherwise.
*/
private function readNumber():JSONToken {
var token:JSONToken = new JSONToken();
token.type = JSONTokenType.NUMBER;
var input:String = "";
if ( ch == '-' ) {
input += '-';
nextChar();
}
if ( !isDigit( ch ) )
{
parseError( "Expecting a digit" );
}
if ( ch == '0' )
{
input += ch;
nextChar();
if ( isDigit( ch ) )
{
parseError( "A digit cannot immediately follow 0" );
}
}
else
{
while ( isDigit( ch ) ) {
input += ch;
nextChar();
}
}
if ( ch == '.' ) {
input += '.';
nextChar();
if ( !isDigit( ch ) )
{
parseError( "Expecting a digit" );
}
while ( isDigit( ch ) ) {
input += ch;
nextChar();
}
}
if ( ch == 'e' || ch == 'E' )
{
input += "e"
nextChar();
if ( ch == '+' || ch == '-' )
{
input += ch;
nextChar();
}
if ( !isDigit( ch ) )
{
parseError( "Scientific notation number needs exponent value" );
}
while ( isDigit( ch ) )
{
input += ch;
nextChar();
}
}
var num:Number = Number( input );
if ( isFinite( num ) && !isNaN( num ) ) {
token.value = num;
return token;
} else {
parseError( "Number " + num + " is not valid!" );
}
return null;
}
/**
* Reads the next character in the input
* string and advances the character location.
*
* @return The next character in the input string, or
* null if we've read past the end.
*/
private function nextChar():String {
return ch = jsonString.charAt( loc++ );
}
/**
* Advances the character location past any
* sort of white space and comments
*/
private function skipIgnored():void {
skipWhite();
skipComments();
skipWhite();
}
/**
* Skips comments in the input string, either
* single-line or multi-line. Advances the character
* to the first position after the end of the comment.
*/
private function skipComments():void {
if ( ch == '/' ) {
nextChar();
switch ( ch ) {
case '/':
do {
nextChar();
} while ( ch != '\n' && ch != '' )
nextChar();
break;
case '*':
nextChar();
while ( true ) {
if ( ch == '*' ) {
nextChar();
if ( ch == '/') {
nextChar();
break;
}
} else {
nextChar();
}
if ( ch == '' ) {
parseError( "Multi-line comment not closed" );
}
}
break;
default:
parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );
}
}
}
/**
* Skip any whitespace in the input string and advances
* the character to the first character after any possible
* whitespace.
*/
private function skipWhite():void {
while ( isWhiteSpace( ch ) ) {
nextChar();
}
}
/**
* Determines if a character is whitespace or not.
*
* @return True if the character passed in is a whitespace
* character
*/
private function isWhiteSpace( ch:String ):Boolean {
return ( ch == ' ' || ch == '\t' || ch == '\n' );
}
/**
* Determines if a character is a digit [0-9].
*
* @return True if the character passed in is a digit
*/
private function isDigit( ch:String ):Boolean {
return ( ch >= '0' && ch <= '9' );
}
/**
* Determines if a character is a digit [0-9].
*
* @return True if the character passed in is a digit
*/
private function isHexDigit( ch:String ):Boolean {
var uc:String = ch.toUpperCase();
return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) );
}
/**
* Raises a parsing error with a specified message, tacking
* on the error location and the original string.
*
* @param message The message indicating why the error occurred
*/
public function parseError( message:String ):void {
throw new JSONParseError( message, loc, jsonString );
}
}
}