//
// Copyright (C) 2006 United States Government as represented by the
// Administrator of the National Aeronautics and Space Administration
// (NASA). All Rights Reserved.
//
// This software is distributed under the NASA Open Source Agreement
// (NOSA), version 1.3. The NOSA has been approved by the Open Source
// Initiative. See the file NOSA-1.3-JPF at the top of the distribution
// directory tree for the complete NOSA document.
//
// THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY
// KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT
// LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL CONFORM TO
// SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR
// A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT
// THE SUBJECT SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT
// DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE SUBJECT SOFTWARE.
//
package gov.nasa.jpf.util.json;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import gov.nasa.jpf.JPFException;
/**
* Lexical analyzer that reads stream and return JSON tokens.
* @author Ivan Mushketik
*/
public class JSONLexer {
// JSON document reader
private Reader reader;
// number of symbol in text
int symbolNumber;
// number line
int lineNumber;
// number of symbol in line
int symbolNumberInLine;
// If parser backtracked to previous symbol
boolean backtracked;
// Last read character
int currentChar;
private static final int STREAM_END = -1;
public JSONLexer(Reader reader) {
this.reader = reader;
backtracked = false;
}
public JSONLexer(String JSONStr) {
this(new StringReader(JSONStr));
}
/**
* Read next token from input stream.
* @return new read token
*/
public Token getNextToken() {
int c;
// Skip whitespaces
do {
c = next();
} while(isSkipChar(c));
if (c == STREAM_END) {
return new Token(Token.Type.DocumentEnd, null);
}
if (c == '{') {
return new Token(Token.Type.ObjectStart, "{");
}
if (c == '}') {
return new Token(Token.Type.ObjectEnd, "}");
}
if (c == '[') {
return new Token(Token.Type.ArrayStart, "[");
}
if (c == ']') {
return new Token(Token.Type.ArrayEnd, "]");
}
if (c == ':') {
return new Token(Token.Type.KeyValueSeparator, ":");
}
if (c == ',') {
return new Token(Token.Type.Comma, ",");
}
if (c == '(') {
return new Token(Token.Type.CGCallParamsStart, "(");
}
if (c == ')') {
return new Token(Token.Type.CGCallParamsEnd, ")");
}
if (c == '\"' || c == '\'') {
return parseString(c);
}
if (Character.isDigit(c) || c == '-') {
back();
return parseNumber();
}
if (isIdentifierStartSymbol(c)) {
back();
return parseIdentifier();
}
// No sutable symbols found
error("Unexpected sybmol");
return null;
}
/**
* Method checks if parser has more input to read
* @return true if scanner has more tokens to read
*/
public boolean hasMore() {
return currentChar != STREAM_END;
}
/**
* Read next symbol from input stream
* @return new read symbol
*/
private int next() {
try {
if (backtracked) {
backtracked = false;
return currentChar;
}
currentChar = reader.read();
symbolNumber++;
symbolNumberInLine++;
if (currentChar == '\n') {
lineNumber++;
symbolNumberInLine = 0;
}
return currentChar;
} catch (IOException ex) {
throw new JPFException("IOException during tokenizing JSON", ex);
}
}
/**
* Backtrack to previous symbol
*/
private void back() {
if (backtracked) {
throw new JPFException("Tried to return twice. Posibly an error. Please report");
}
backtracked = true;
}
// Scaner doesn't backtrack before call this method
private Token parseString(int delimiter) {
StringBuilder result = new StringBuilder();
int c;
while((c = next()) != delimiter) {
if (c == '\\') {
result.append((char) readEscapedSymbol());
} else {
result.append((char) c);
}
}
return new Token(Token.Type.String, result.toString());
}
private int readEscapedSymbol() {
int escaped = next();
int res = -1;
switch(escaped) {
case '\"':
case '\\':
case '/':
res = escaped;
break;
case 'b':
res = '\b';
break;
case 'f':
res = '\f';
break;
case 'n':
res = '\n';
break;
case 'r':
res = '\r';
break;
case 't':
res = '\t';
break;
// Extract hexadecimal Unicode symbol (\\uXXXX)
case 'u': {
String r = "";
int i = 0;
int c;
while (hexadecimalChar(c = next()) && i < 4) {
r += (char) c;
i++;
}
// Unicode escape consists of 4 hexadecimal symbols
if (i < 4) {
error("Escaped Unicode symbol should consist of 4 hexadecimal digits");
}
back();
res = Integer.parseInt(r, 16);
}
break;
default:
error("Illegal excape");
break;
}
return res;
}
private Token parseNumber() {
StringBuilder sb = new StringBuilder();
int c = next();
// '-' symbol is not obligatory
if (c == '-') {
sb.append('-');
} else {
// We read unnecessary symbol, need to bactrack
back();
}
c = next();
// Integer part of digit is either '0' or '1'..'9' and digits
if (c == '0') {
sb.append('0');
} else {
back();
sb.append(readDigits());
}
c = next();
// "float part"
if (c == '.') {
sb.append('.');
sb.append(readDigits());
} else {
back();
}
c = next();
if (c == 'e' || c == 'E') {
sb.append((char) c);
c = next();
if (c == '+' || c == '-') {
sb.append((char) c);
} else {
back();
}
sb.append(readDigits());
} else {
back();
}
return new Token(Token.Type.Number, sb.toString());
}
/**
* Read at least one digit
* @return String that represents read number
*/
private String readDigits() {
StringBuilder sb = new StringBuilder();
int c;
int n = 0;
while (Character.isDigit(c = next())) {
sb.append((char) c);
n++;
}
if (n == 0) {
error("Expected not empty sequence of digits");
}
back();
return sb.toString();
}
private Token parseIdentifier() {
StringBuilder result = new StringBuilder();
int c = next();
while (Character.isJavaIdentifierPart(c)) {
result.append((char) c);
c = next();
}
back();
return new Token(Token.Type.Identificator, result.toString());
}
private boolean isIdentifierStartSymbol(int c) {
return Character.isJavaIdentifierStart(c);
}
private boolean isSkipChar(int currentChar) {
return Character.isSpaceChar(currentChar);
}
private void error(String string) {
throw new JPFException(string + " '" + (char) currentChar + "' charCode = " + currentChar +
"; in line " + lineNumber + " pos " + symbolNumberInLine);
}
private boolean hexadecimalChar(int i) {
return Character.isDigit(i) || (i <= 'F' && i >= 'A') || (i <= 'f' && i >= 'a');
}
int getLineNumber() {
return lineNumber;
}
int getCurrentPos() {
return symbolNumberInLine;
}
}