package org.basex.query.util.json;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import org.basex.query.QueryException;
import org.basex.util.InputInfo;
import org.basex.util.InputParser;
import org.basex.util.TokenBuilder;
import org.basex.util.Util;
/**
* <p>This class converts a JSON document to a tree representation.</p>
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
final class JSONParser extends InputParser {
/** Error: invalid character. */
private static final String INVALID = "Invalid character: \"%\"";
/** Error: invalid and expected character. */
private static final String INVALEXP = "Char \"%\" found, % expected";
/** Token builder. */
private final TokenBuilder tb = new TokenBuilder();
/** Input info. */
private final InputInfo input;
/**
* Constructor.
* @param q query
* @param ii input info
*/
public JSONParser(final byte[] q, final InputInfo ii) {
super(string(q));
input = ii;
}
/**
* Parses the input.
* @return resulting node
* @throws QueryException query exception
*/
public JStruct parse() throws QueryException {
JStruct root = object();
if(root == null) root = array();
if(root == null) error(INVALEXP, curr(), "\"{\" or \"[\"");
skipWS();
if(more()) error(INVALEXP, curr(), "end of file");
return root;
}
/**
* Returns an object.
* @return success flag
* @throws QueryException query exception
*/
private JObject object() throws QueryException {
if(!wsConsume('{')) return null;
final JObject o = new JObject();
do {
final byte[] key = str();
if(key == null) {
if(o.size() != 0) error(INVALEXP, curr(), '"');
break;
}
wsCheck(':');
o.add(key, value(true));
} while(wsConsume(','));
wsCheck('}');
return o;
}
/**
* Parses an array.
* @return success flag
* @throws QueryException query exception
*/
private JArray array() throws QueryException {
if(!wsConsume('[')) return null;
final JArray a = new JArray();
do {
final JValue val = value(a.size() != 0);
if(val != null) a.add(val);
} while(wsConsume(','));
wsCheck(']');
return a;
}
/**
* Parses a value.
* @param mand mandatory flag
* @return success flag
* @throws QueryException query exception
*/
private JValue value(final boolean mand) throws QueryException {
skipWS();
final char ch = curr();
if(digit(ch) || ch == '-') return new JNumber(number());
if(ch == '"') return new JString(str());
if(ch == '{') return object();
if(ch == '[') return array();
if(ch == 't' || ch == 'f') return new JBoolean(bool());
if(ch == 'n') {
for(final byte b : NULL) check((char) b);
return new JNull();
}
if(mand) error(INVALEXP, curr(), '"');
return null;
}
/**
* Parses an string.
* @return resulting string
* @throws QueryException query exception
*/
private byte[] str() throws QueryException {
if(!wsConsume('"')) return null;
tb.reset();
while(more()) {
int ch = consume();
if(ch == '"') return tb.finish();
if(ch == '\\') {
ch = consume();
if(ch == 'u') {
int i = 0;
for(int s = 0; s < 4; s++) {
ch = consume();
i <<= 4;
if(ch >= '0' && ch <= '9') i += ch - 0x30;
else if(ch >= 'A' && ch <= 'F') i += ch - 0x37;
else if(ch >= 'a' && ch <= 'f') i += ch - 0x57;
else error(INVALID, ch, "hex digit");
}
ch = i;
} else if(ch == 'b') {
ch = '\b';
} else if(ch == 'f') {
ch = '\f';
} else if(ch == 'n') {
ch = '\n';
} else if(ch == 'r') {
ch = '\r';
} else if(ch == 't') {
ch = '\t';
} else if("\\\"/".indexOf(ch) == -1) {
error(INVALID, "\\" + (char) ch);
}
}
tb.add(ch);
}
throw error(INVALEXP, 0, '"');
}
/**
* Parses an number.
* @return resulting number
* @throws QueryException query exception
*/
private byte[] number() throws QueryException {
tb.reset();
if(curr() == '-') tb.add(consume());
if(curr() == '0') {
tb.add(consume());
} else {
digits();
}
if(curr() == '.') {
tb.add(consume());
digits();
}
if(curr() == 'e' || curr() == 'E') {
tb.add(consume());
if(curr() == '+' || curr() == '-') tb.add(consume());
digits();
}
return tb.finish();
}
/**
* Parses a boolean.
* @return resulting number
* @throws QueryException query exception
*/
private byte[] bool() throws QueryException {
if(curr() == 't') {
for(final byte b : TRUE) check((char) b);
return TRUE;
}
for(final byte b : FALSE) check((char) b);
return FALSE;
}
/**
* Consumes digits.
* @throws QueryException query exception
*/
private void digits() throws QueryException {
if(!digit(curr())) throw error(INVALEXP, curr(), "digit");
do tb.add(consume()); while(digit(curr()));
}
/**
* Consumes leading whitespaces and the specified character.
* @param c character to consume
* @return true if token was found
*/
private boolean wsConsume(final int c) {
skipWS();
return consume(c);
}
/**
* Consumes consecutive whitespace characters.
*/
private void skipWS() {
while(more()) {
final int c = curr();
if(c == 0 || c > ' ') break;
++qp;
}
}
/**
* Skips whitespaces, raises an error if the specified character cannot be
* consumed.
*
* @param ch character to be found
* @throws QueryException query exception
*/
private void wsCheck(final char ch) throws QueryException {
if(!wsConsume(ch)) error(INVALEXP, curr(), "\"" + ch + '"');
}
/**
* Raises an error if the specified character cannot be consumed.
*
* @param ch character to be found
* @throws QueryException query exception
*/
private void check(final char ch) throws QueryException {
if(!consume(ch)) error(INVALEXP, curr(), "\"" + ch + '"');
}
/**
* Raises an error with the specified message.
* @param msg error message
* @param ext error details
* @return build exception
* @throws QueryException query exception
*/
private QueryException error(final String msg, final Object... ext)
throws QueryException {
final int[] lc = new InputInfo(this).lineCol();
throw JSONPARSE.thrw(input, lc[0], lc[1], Util.inf(msg, ext));
}
}