package nbtool.data.json; import java.util.HashMap; import java.util.Map; import nbtool.data.json.Json.JsonValue; import nbtool.data.json.Json.JsonValueType; import nbtool.util.Debug; public class JsonParser { private String text; public int pos; public int position() { return pos; } protected JsonParser(String text, int pos) { this.text = text; this.pos = pos; } public JsonValue parse() throws JsonParseException { Token tok = nextToken(); return _parse(tok); } public static class JsonParseException extends Exception { public String parseText; public int pos; protected JsonParseException(String reason, int pos, String text) { super(reason); this.parseText = text; this.pos = pos; } } public class UnexpectedTokenException extends JsonParseException { protected UnexpectedTokenException(Token problem) { super(String.format("unexpected token %s at %d", problem, problem.start), problem.start, text); } } public class MissingTokenException extends JsonParseException { protected MissingTokenException(TokenType wanted, Token found) { super(String.format("missing token %s at %d", wanted.describe(), found.start), found.start, text); } } public class UnexpectedCharacterException extends JsonParseException { public char unexpected; protected UnexpectedCharacterException(char c, int pos) { super(String.format("unexpected character '%c' at %d", c, pos), pos, text); this.unexpected = c; } } public class MissingCharacterException extends JsonParseException { public char expected; protected MissingCharacterException(char c, int pos) { super(String.format("expected character '%c' at %d", c, pos), pos, text); this.expected = c; } } public static enum TokenType { ARRAY_START('['), ARRAY_END(']'), SEPARATOR(','), OBJECT_START('{'), OBJECT_DIVIDER(':'), OBJECT_END('}'), /* parsed trings may be delimited with the single quote ' as well * but Json objects serialized using this library will always delimit with " */ STRING('"'), NUMBER(null); public final Character CHARACTER; private TokenType(Character c) { this.CHARACTER = c; } public String describe() { return String.format("type-%s std_char-'%s'", toString(), "" + CHARACTER); } } private static class Token { TokenType type; String val; int start; int after; @Override public String toString() { return String.format("[%s{%s}{%d,%d}]", type, val, start, after); } } private static final Map<Character, TokenType> SPECIAL = new HashMap<>(); static { SPECIAL.put('[', TokenType.ARRAY_START); SPECIAL.put(']', TokenType.ARRAY_END); SPECIAL.put(',', TokenType.SEPARATOR); SPECIAL.put('{', TokenType.OBJECT_START); SPECIAL.put(':', TokenType.OBJECT_DIVIDER); SPECIAL.put('}', TokenType.OBJECT_END); SPECIAL.put('\'', TokenType.STRING); SPECIAL.put('"', TokenType.STRING); } private static final Map<String, JsonValue> RESERVED = new HashMap<>(); static { RESERVED.put("null", Json.NULL_VALUE); RESERVED.put("true", JsonBoolean.TRUE); RESERVED.put("false", JsonBoolean.FALSE); } private JsonValue _parse(Token tok) throws JsonParseException { switch(tok.type) { case ARRAY_END: { throw new UnexpectedTokenException(tok); } case ARRAY_START: { JsonArray array = new JsonArray(); Token first = peekToken(); if (first.type == TokenType.ARRAY_END) { nextToken(); //Consume ARRAY_END return array; } for(;;) { Token value = nextToken(); array.add( _parse(value) ); Token after = nextToken(); if (after.type == TokenType.ARRAY_END) break; if (after.type != TokenType.SEPARATOR) throw new MissingTokenException(TokenType.SEPARATOR, after); } return array; } case NUMBER: { return new JsonNumber(tok.val); } case OBJECT_DIVIDER: { throw new UnexpectedTokenException(tok); } case OBJECT_END: { throw new UnexpectedTokenException(tok); } case OBJECT_START: { JsonObject object = new JsonObject(); Token first = peekToken(); if (first.type == TokenType.OBJECT_END) { //Logger.println("emtpy object"); nextToken(); //Consume OBJECT_END return object; } for (;;) { readObjectPair(object); Token after = nextToken(); if (after.type == TokenType.OBJECT_END) break; if (after.type != TokenType.SEPARATOR) throw new MissingTokenException(TokenType.SEPARATOR, after); } return object; } case SEPARATOR: { throw new UnexpectedTokenException(tok); } case STRING: { if (tok.val.trim().isEmpty()) { throw new JsonParseException("empty STRING token: " + tok, tok.start, text); } if (RESERVED.containsKey(tok.val)) { return RESERVED.get(tok.val); } String string = tok.val; char start = string.charAt(0); if (SPECIAL.containsKey(start)) { assert(SPECIAL.get(start) == TokenType.STRING); assert(string.endsWith("" + start)); string = string.substring(1, string.length() - 1); } return new JsonString(string); } default: throw new JsonParseException("unknown token " + tok, tok.start, text); } } private void readObjectPair(JsonObject object) throws JsonParseException { Token keyTok = nextToken(); JsonValue key = _parse(keyTok); if (key.type() != JsonValueType.STRING) throw new JsonParseException("Object key MUST be string", keyTok.start, text); Token divTok = nextToken(); if (divTok.type != TokenType.OBJECT_DIVIDER) throw new MissingTokenException(TokenType.OBJECT_DIVIDER, divTok); Token valTok = nextToken(); JsonValue value = _parse(valTok); object.put( (JsonString) key, value); } private void skip() { boolean comment = false; for (; pos < text.length(); ++pos) { if (comment) { if (text.charAt(pos) == '\n') comment = false; } else { if (isWhitespace(pos)) continue; if (text.charAt(pos) == '#') { comment = true; continue; } return; } } } /* * Consumes whitespace leading up to next token but does not consume token itself. * */ private Token peekToken() throws JsonParseException { skip(); if (pos >= text.length() || text.charAt(pos) == '\0') { throw new JsonParseException("expected token but reached end of input", pos, text); } char c = text.charAt(pos); TokenType type = SPECIAL.get(c); if ( type != null ) { if (type != TokenType.STRING) { Token ret = new Token(); ret.type = type; ret.val = null; ret.start = pos; ret.after = pos + 1; return ret; } else { //Escaped string. int end = pos + 1; for (; end < text.length() && !stringTerminated(end, c); ++end ); if ( end >= text.length() ) throw new MissingCharacterException(c, end); assert(text.charAt(end) == c); Token ret = new Token(); ret.type = TokenType.STRING; //Must include strchar! Otherwise "null" cannot be differentiated from null . ret.val = text.substring(pos, end + 1); ret.start = pos; ret.after = end + 1; return ret; } } //Number token – all numbers start with either a minus or a digit. if (c == '-' || Character.isDigit(c)) { int after = pos + 1; for (; after < text.length() && isNumberChar(after); ++after); Token ret = new Token(); ret.type = TokenType.NUMBER; ret.val = text.substring(pos, after); ret.start = pos; ret.after = after; return ret; } //Unescaped string (or possibly reserved string) if (Character.isLetter(c)) { int after = pos + 1; for (; after < text.length() && !isWhitespace(after) && !SPECIAL.containsKey(text.charAt(after)); ++after); Token ret = new Token(); ret.type = TokenType.STRING; ret.val = text.substring(pos, after); ret.start = pos; ret.after = after; return ret; } throw new UnexpectedCharacterException(c, pos); } private Token nextToken() throws JsonParseException { Token ret = peekToken(); pos = ret.after; return ret; } private boolean isWhitespace(int i) { char c = text.charAt(i); return Character.isWhitespace(c); } private boolean isNumberChar(int i) { char c = text.charAt(i); return Character.isLetterOrDigit(c) || c == '.'; } private boolean stringTerminated(int p, char strchar) { return text.charAt(p) == strchar && text.charAt(p - 1) != '\\'; } /* * For immediate testing – long term testing must be done in the unit tests ( nbtool.term.units ) * */ public static void main(String[] args) throws JsonParseException { /* String line = "[hello,null,true,{}, \"hello there\", {val:45}]"; JsonParser parser = new JsonParser(line, 0); Logger.println("go...."); JsonValue val = parser.parse(); Logger.println("VALUE:" + val); JsonObject obj = new JsonObject(); obj.put("key", new JsonString("value")); obj.put("array", new JsonNumber("5000")); obj.put("reuse", new JsonObject()); String serd = obj.print(); JsonValue val2 = Json.parse(serd); Logger.printf("--------------------------------\n%s", val2.print()); */ /* JsonArray array = new JsonArray(); array.add(new JsonString("fellow")); array.add(new JsonString("cow")); array.add(new JsonNumber("5000")); array.add(new JsonString("thing")); JsonObject obj = new JsonObject(); obj.put("key1", new JsonNumber("50")); obj.put("key2", new JsonString("the string")); array.add(obj); Logger.println(array.print() + "\n"); */ JsonArray outer = new JsonArray(); JsonArray inner = Json.array(); outer.add(Json.NULL_VALUE); inner.add(Json.NULL_VALUE); outer.add(inner); Debug.plain(outer.print() + "\n"); String var = "#some comments\n {word : #cmmnt \n null}"; Debug.plain(Json.parse(var).print()); } }