Здравствуйте, уважаемые читатели Хабра. В данной статье Вы узнаете, как написать свой JSON-парсер (с жуками и фичами). Созданный парсер будет преобразовывать последовательность текстовых символов в well-formed JSON-объект, который будет представлять JSON-Документ. JSON-документ — это последовательность символов, которая правильно и неукоснительно выполняет правила грамматики языка. Существуют несколько грамматик и RFC, для описания правильной последовательности текста в формате JSON. Для данного парсера воспользуемся спецификацией RFC 8259.
Мотивация
Во-первых, избавиться от некоторых гугловских библиотек, используемых только для разбора JSON текста.
Во-вторых, написать свой, красивый простой API с богатым набором методов, конструкторов и прочих вещей.
В-третьих, изменяемость. Внесение изменений в своё творение.
Элементы JSON.
Итак, что будет вовзращать наш парсер? Очевидно, что одного ответа на вопрос: «Данный текст соответствует JSON-формату?» будет недостаточно. Нам необходимо извлечь содержимое JSON, если таковое имеется. Следовательно, куда сохранить, как хранить, как реализовывать, как потом обращаться с ним? Так много вопросов. Будем действовать последовательно.
Обратимся к RFC. Согласно документу, есть 4 примитивных типов, и 2 структурных (объекты и массивы). Начнём с не-структруных типов. Для каждого типа создадим свой собственный класс, который будет возвращать значение соответствующего типа. Все они будут наследоваться от общего абстрактного класса JsonElement, представляющий элементарное значение в JSON-е.
public abstract class JsonElement<T> { //T - конкретный тип данного в JSON. public abstract T getValue(); //Значение указанного типа в тексте. //Возвращаем строковый вариант самого значения @Override public String toString(){ return getValue().toString(); } }
Класс JsonElement абстрактный, поэтому каждый его дочерний подкласс, который будет содержать конкретику, должен реализовать метод getValue.
Начнём с булевого типа (boolean). Напишем класс JsonBoolean производный от JsonElement<Boolean>.
public class JsonBoolean extends JsonElement<Boolean> { //Значение типа - Boolean private boolean v; //По умолчанию = false. public JsonBoolean(){ this.v = false; } //передать выражение логического типа. public JsonBoolean(Boolean b){ this.v = b; } //Мнемонический конструктор. //t = true (t stands for true). Как сокращение. //other = false. (любой символ, кроме 't'). public JsonBoolean(Character c){ this.v = (c == 't'); } @Override public Boolean getValue() { return v; } }
Далее, напишем такой же класс для числового типа (Number). Заметим, что в JSON можно описывать как целые числа, так и действительные числа с десятичной точкой. Для отделения целых чисел, от чисел с точкой, определим два класса. Первый класс JsonNumber будет обозначать целые числа. Его код абсолютно идентичен, за исключением конструкторов и схемы наследования.
public class JsonNumber extends JsonElement<Number> { // Числовой тип - Number. private Number n; //Не знаем точно, какой именно из числовых типов, поэтому Number. public JsonNumber(Number n){ this.n = n; } @Override public Number getValue() { return n; } }
Код же для класса действительных чисел, JsonRealNumber ещё проще:
public class JsonRealNumber extends JsonNumber { // Включает в себя JsonNumber. //Здесь можно использовать конкретный тип для чисел с плавающей запятой. //Float или Double. public JsonRealNumber(Float n){ super(n); } public JsonRealNumber(Double n) { super(n); } }
Остаётся строковый тип JsonString и тип JsonNull для значения null (да, для null — написали аж отдельный класс.). Ниже приведен код данных классов.
public class JsonString extends JsonElement<String> { private String val; public JsonString(String v){ this.val = v; } @Override public String getValue() { return val; } // Не надо лишний раз вызывать getValue(), у нас уже есть строка. @Override public String toString(){ return val; } } //Определили в отдельном файле JsonNull.java public class JsonNull extends JsonElement<String> { @Override public String getValue() { return "null"; //Просто возвращаем в виде строки "null" } }
Объясним по поводу getValue() у типа JsonNull. Как Вы уже заметили, значение строки в виде «null» типа JsonString совпадает со значением типа JsonNull. Отличить их можно по конкретному типу JsonNull.
Теперь перейдем к структурным типам. Самый простой — это массив, который содержит в виде списка коллекцию элементов JsonElement. Для простоты, класс JsonArray (для массива), лишь оборачивает работу с ArrayList<JsonElement>. Его код приведён далее.
/* JsonArray - как List of JsonElements. */ public class JsonArray extends JsonElement<List<JsonElement>> { private List<JsonElement> elements; private int c; // 'c' stands for count (число элементов). private String pName; // имя свойства, по которому массив доступен. public JsonArray(){ this.elements = new ArrayList<>(); this.c = 0; this.pName = null; } public JsonArray(List<JsonElement> elements){ this.elements = elements; this.c = elements.size(); this.pName = null; } //Меняем ссылку (НЕ addElements)! public void setElements(List<JsonElement> elements) { this.elements = elements; } public <T> void add(JsonElement<T> e){ elements.add(e); c++; } public void setPropertyName(String pName) { this.pName = pName; } public String getPropertyName(){ return pName; } public ArrayList<JsonElement> getElements() { return elements; } //getCount(). public int getC() { return c; } @Override public List<JsonElement> getValue() { return elements; }
Остаётся теперь последний тип — объекты JSON. Структура объекта содержит пары «имя-значение», где значение может быть либо структурным типом (объект или массив), либо обычным типом, которые уже перечислялись выше. Имена — суть ключи. Тип ключей — строка. Напишем отдельный класс для хранения последовательности таких пар, и их соответствующего вывода на экран (в консоль, текстовый файл и т.д.).
/* LinkedHashMap - сохраняет порядок вставки элементов. Нужная вещь, при последовательном проходе и извлечений пар. TKey = String. (Тип ключа - строки). TValue - V. (Пока оставили произвольный). */ public class StrLinkedHashMap<V> extends LinkedHashMap<String, V> { //Метод, для вывода значений у пар. @Override public String toString(){ Set<String> keys = keySet(); //Не рассматриваем параллельные потоки. //(Непотокобезопасен! Используйте StringBuffer) StringBuilder sb = new StringBuilder(); sb.append("{\n\t"); // имена и значения объекта заключены в фигурные скобки. Iterator<String> t = keySet().iterator(); while(t.hasNext()){ String k = t.next(); sb.append(k+" : "+get(k).toString()); if(t.hasNext()) sb.append(",\n\t"); } sb.append("}"); return sb.toString(); } }
Затем воспользуемся новым типом, для определения типа-объекта JSON. Определим JSON-объект в виде класса JsonObject.
public class JsonObject extends JsonElement<StrLinkedHashMap<JsonElement>> { private StrLinkedHashMap<JsonElement> table; // Содержимое public JsonObject(){ this.table = new StrLinkedHashMap<>(); } public void Put(String k, JsonElement id){ this.table.put(k,id); } //inherits from LinkedHashMap. // Получить значение по ключу. // В данном случае по имени свойства. // Метод получает значение из текущей коллекции пар, по ключу k. // Не работает со вложенными объектами (свойствами). public JsonElement Get(String k){//get element on the root level. return table.get(k); } //Синоним для метода Get(String k). public JsonElement getProperty(String pname){ return table.get(pname); } //get Element by path using path dot notation. // Работает со вложенными объектами. public JsonElement getElement(String path){ ArrayList<String> p = new ArrayList<>(); //Разделить строку на слова по разделителю точка '.' Pattern regex = Pattern.compile("\\w([^.]*)"); Matcher m = regex.matcher(path); while(m.find()) p.add(m.group()); StrLinkedHashMap<JsonElement> cnode = table;//ROOT. Корень. JsonElement n = null; //Если имя свойства без точек -> извлечь непосредственно. if(p.size() == 0) return getProperty(path); // Двигаемся вглубь, последовательно обращаясь к "A.B.C" for(String c : p){ n = cnode.get(c); if(n == null) //Ничего вообще нет (даже null в тексте не указали) return n; // будет null в коде. try {//is interior node //cnode - текущий объект по вложенности. // Переходим к следующему, если не получилось, то извлекаем у текущего. cnode = (StrLinkedHashMap<JsonElement>) n.getValue(); // isTable. }catch (ClassCastException e){//that was leaf. (не структурный тип). return cnode.get(c); //Извлечь значение данного типа у текущего объекта. } } return n; } //Значение - коллекция пар "имя" : "значение". @Override public StrLinkedHashMap<JsonElement> getValue() { return table; } //Нерекурсивно, явно с использованием стэка, выводим построчно всё содержимое, //включая вложенные объекты и массивы. @Override public String toString(){ StringBuilder sb = new StringBuilder(); LinkedStack<Triple<String, JsonElement,String>> S = new LinkedStack<>();//entity LinkedStack<Integer> T = new LinkedStack<>();//count of tabs (\t symbol) int tabs = 0;//current count of tabs T.push(0); S.push(new Triple<>("", this, "")); while(!S.isEmpty()){ tabs = T.top(); T.pop(); _addTabs(sb,tabs); String prop = S.top().getV1(); sb.append(prop); JsonElement e = S.top().getV2(); String c = S.top().getV3(); S.pop(); if(e instanceof JsonObject){ //WRITE BOUND OF OBJECT AND INCREMENT(tabs) sb.append('{').append('\n'); T.push(tabs); S.push(new Triple<>("", new JsonLiteral("}"), c)); //ADD <key, value> pair to the STACK. JsonObject ob = (JsonObject)e; Iterator<String> ks = ob.table.keySet().iterator(); //FIRST ITERATION (put to stack entry without comma) if(ks.hasNext()) { String p = ks.next(); String v1 = "\"" + p + "\"" + " : "; S.push(new Triple<>(v1, ob.getProperty(p), "")); T.push(tabs + 1); } //put others with comma while(ks.hasNext()){ String p = ks.next(); String v1 = "\"" + p + "\"" + " : "; S.push(new Triple<>(v1, ob.getProperty(p), ",")); T.push(tabs + 1); } } else if(e instanceof JsonArray){ ArrayList<JsonElement> arr = ((JsonArray)e).getValue(); int s = arr.size(); sb.append('[').append('\n'); S.push(new Triple<>("", new JsonLiteral("]"), c)); T.push(tabs); //FIRST ITERATION JsonElement el = arr.get(s - 1); s--; S.push(new Triple<>("", el, "")); T.push(tabs + 1); //put others with comma. while(s != 0){ el = arr.get(s - 1); s--; S.push(new Triple<>("", el, ",")); T.push(tabs + 1); } } else if(e instanceof JsonLiteral || e instanceof JsonNull || e instanceof JsonNumber || e instanceof JsonBoolean){ sb.append(e.getValue().toString()).append(c).append('\n'); } else if(e instanceof JsonString){ sb.append('\"').append(((JsonString) e).getValue()).append('\"').append(c).append('\n'); } } return sb.toString(); } //Вспомогательный метод, добавляет табы. private void _addTabs(StringBuilder sb, int tabs){ int k = 0; while(k < tabs){ sb.append('\t'); k++; } } }
Для вывода всего содержимого у объекта, был определён вспомогательный тип — JsonLiteral. Он лишь нужен для уточнения и разделения последовательности содержимого. Его код представлен ниже.
public class JsonLiteral extends JsonString { public JsonLiteral(String v) { super(v); } }
Состояния парсера.
Парсер принимает на вход последовательность текстовых символов. Он будет возвращать содержимое JSON, в виде объекта-JSON. Т.е. это будет экземпляр класса JsonObject. Поскольку работать придётся со вложенными структурами, то необходимо будет использовать стэки. Распознанную лексему тоже придётся сохранять на время где-нибудь. И надо будет также пробрасывать свои исключения (ошибки), при обнаружении несоответствия к JSON-у.
Начнём с простого, напишем первоначальный код класса парсера, SimpleJsonParser.
public class SimpleJsonParser { private JsParserState state; //Текущее состояние парсера. private char[] buf; //Буфер для входного потока. private int bsize; //Размер буфера private int bufp; //Текущая позиция в буфере. private int line; //line, col - позиция в текстовом файле. private int col; private JsonElement curVal; //текущий распознанный элемент. //Создать с буфером размера bsize. //Начальное состояние - START, позиция в буфере = 0, //позиция в файле - первая строка, нулевой столбец. (1, 0). public SimpleJsonParser(int bsize){ this.state = JsParserState.START; //START - начальное состояние. this.buf = new char[bsize]; this.bsize = bsize; this.bufp = 0; this.line = 1; this.col = 0; } //Размер буфера по умолчанию - 255 символов. public SimpleJsonParser(){ this(255); } //...other code. }
а для исключений определим новый тип исключений — JsonParseException.
//Расширяемся от RuntimeException - // - не надо проверять при компиляции. // - не надо обёртывать try/catch (в Runtime упадёт). public class JsonParseException extends RuntimeException { public JsonParseException(String message, Throwable throwable){ super(message, throwable); } }
Итак, входом будет последовательность символов, представленная типом InputStream. Определим базовую возможность, ввести имя файла, с которого будем читать. Для этого напишем сразу три метода в классе.
public class SimpleJsonParser { //Поля и конструктора.... //Получить по абсолютному или относительному пути экземпляр File. public JsonObject parse(String fileName) { File f = new File(fileName); return parse(f); } public JsonObject parse(File fl){ JsonObject result = null; try(FileInputStream f = new FileInputStream(fl.getAbsolutePath()); ){ result = parseStream(f); } catch (FileNotFoundException e){ System.out.println(e.getMessage()); flushBuf(); } catch (IOException e){ System.out.println(e.getMessage()); this.state = JsParserState.START; //Исключение - назад к начальному состоянию. flushBuf(); this.col = 0; this.line = 1; return null; } return result; } //Можно вызвать напрямую, передав InputStream, // или же воспользоваться первым методом, для чтения из файла. public JsonObject parseStream(InputStream in){ //TODO: обработка входа IN. return null; } //Сбросить содержимое буфера, установив все символы в ноль. private void flushBuf(){ bufp = 0; for(int i = 0; i < this.buf.length; i++){ this.buf[i] = '\u0000'; } } }
Поскольку предполагается работа с текстовым файлом, то InputStream не подойдёт, поскольку он работает с бинарниками. Нужен более конкретный тип — InputStreamReader, который может работать с текстом.
Для сохранения текущего контекста вложенности, будем сохранять последовательности вложенных массивов, объектов, имён свойств и текущих вершин (структурных типов) в четыре соответствующих стэка: J_OBJS, J_ARRS, J_ROOTS, props. Обработка текста будет идти до тех пор, пока не сформируется полноценный JSON-объект со всем вложенным в него содержимым (обозначим как состояние CLOSEROOT), либо до первой грамматической ошибки (обозначим как состояние ERR). В итоге, конечный результат будет лежать на вершине стэка J_OBJS, поскольку парсер возвращает JsonObject. Итерацию напишем в отдельном методе iterate.
public JsonObject parseStream(InputStream in){ LinkedStack<JsonObject> J_OBJS = new LinkedStack<>(); LinkedStack<JsonArray> J_ARRS = new LinkedStack<>(); LinkedStack<JsonElement> J_ROOTS = new LinkedStack<>(); LinkedStack<String> props = new LinkedStack<>(); //InputStreamReader от InputStream. try(InputStreamReader ch = new InputStreamReader(in)){ this.state = JsParserState.START;// before read set parser to the start state. this.col = 0; this.line = 1; flushBuf(); //Заранее ставим в начало перед обработкой. //Основной цикл. while(this.state != JsParserState.CLOSEROOT && this.state != JsParserState.ERR) iterate(J_OBJS, J_ARRS, J_ROOTS, props, ch); if(this.state == JsParserState.ERR) return null; //Если ошибка, то null. } catch (IOException e){ //Ошибка чтения - null (см. выше). System.out.println(e.getMessage()); System.out.println("At (" + line + ":" + col + ")"); flushBuf(); return null; } return J_OBJS.top(); }
Теперь распишем метод iterate со всеми его частями и вспомогательными методами.
private void iterate(LinkedStack<JsonObject> J_OBJS, LinkedStack<JsonArray> J_ARR, LinkedStack<JsonElement> J_ROOTS, LinkedStack<String> props, InputStreamReader r) throws IOException{ int c = (int)' '; JsonObject cur_obj = null; while(c == ' ' || c == '\n' || c == '\r' || c == '\t') { //skip spaces c = getch(r); } //JSON_OBJ -> { PROPS } | { }. if(this.state == JsParserState.AWAIT_PROPS_OR_END_OF_OBJ && c == '}' && ungetch('}') == 0) err('}', "available space for '}' but OutOfMemory!"); else if(this.state == JsParserState.AWAIT_PROPS_OR_END_OF_OBJ && c == '}'){ // side effect from previous if [ ungetch('}') call]! this.state = JsParserState.NEXT_VALUE; // just goto NEXT_VALUE where all checks. } else if(this.state == JsParserState.AWAIT_PROPS_OR_END_OF_OBJ && ungetch((char) c) == 0) // ungetch call produces side effect for next else if branch err(c, "available space for '"+(char) c +"' but OutOfMemory!"); else if(this.state == JsParserState.AWAIT_PROPS_OR_END_OF_OBJ) this.state = JsParserState.AWAIT_PROPS; else if(this.state == JsParserState.START && c != '{') //Json object must starts with '{' err(c, "{"); else if(this.state == JsParserState.START){ // state = Start, c == '{' J_OBJS.push(new JsonObject()); J_ROOTS.push(J_OBJS.top()); this.state = JsParserState.AWAIT_PROPS_OR_END_OF_OBJ; } // MEMBER -> >" propName " : PROPVALUE | STRINGVAL -> >" symbols ". else if((this.state == JsParserState.AWAIT_PROPS || this.state == JsParserState.AWAIT_STRVALUE) && c != '\"') err(c, "\""); else if(this.state == JsParserState.AWAIT_PROPS){ // state == AWAIT_PROPS, c == '"' [ trans_from(Start, '{') ] c = getFilech(r); //read character directly from file. int l = 0; while(c != '\"' && bufp < bsize) { //read all content until '"' char (end of the string) while buffer available. if(c == '\\') c = getEscaped(r); l++; buf[bufp++] = (char)c; c = getFilech(r); } if(bufp >= bsize && c != '\"') { //too long string (buffer exceeded) err(c, "available space for \'"+(char)c +"'\' or EOL (\") symbol"); return; } props.push(new String(buf, 0, l)); flushBuf(); this.state = JsParserState.READ_PROPNAME; } else if(this.state == JsParserState.AWAIT_STRVALUE){ c = getFilech(r); //read character. int l = 0; while(c != '\"' && bufp < bsize) { if(c == '\\') c = getEscaped(r); l++; buf[bufp++] = (char)c; c = getFilech(r); } if(bufp >= bsize && c != '\"') { err(c, "available space for \'"+(char)c +"'\' or EOL (\") symbol"); return; } this.state = JsParserState.READ_STRVALUE; this.curVal = new JsonString(new String(buf, 0, l)); flushBuf(); } else if(this.state == JsParserState.READ_PROPNAME && c != ':') err(c,":"); else if(this.state == JsParserState.READ_PROPNAME){ // c == ':' name value separator was read. this.state = JsParserState.COLON; } else if(this.state == JsParserState.EMPTY_OR_NOT_ARR && c == ']' && ungetch(']') == 0){ err(']', "available space for ']' but OutOfMemory!"); } else if(this.state == JsParserState.EMPTY_OR_NOT_ARR && c == ']'){ // side effect from previous else if [ungetch() call!] this.state = JsParserState.NEXT_VALUE; } else if(this.state == JsParserState.EMPTY_OR_NOT_ARR && ungetch((char) c) == 0){ // c != ']' err(c, "available space for '"+(char)c+"' but OutOfMemory!"); } else if(this.state == JsParserState.EMPTY_OR_NOT_ARR){ this.state = JsParserState.COLON; } //FIRST SYMBOL OF VALUE (AFTER SEPARATOR) else if(this.state == JsParserState.COLON){ //System.out.println("Property: "+props.top()+ " symbol \'"+(char)c+"\'"); switch (c){ case '\"':{ this.state = JsParserState.AWAIT_STRVALUE; if( ungetch((char) c) == 0) err(c, "available space but OutOfMemory!"); break; } case '{':{ this.state = JsParserState.START; if(ungetch((char) c) == 0) err(c, "available space but OutOfMemory!"); break; } case '[': { J_ARR.push(new JsonArray()); J_ROOTS.push(J_ARR.top()); this.state = JsParserState.EMPTY_OR_NOT_ARR; break; } default: { int l = 0; //CHECK that rvalue is not consists of token symbols ( ']' '}' ',' ':' '[' '{', EOF) while(bufp < bsize && (c != ' ' && c != '\n' && c != '\r' && c != '\t') && (c != '}' && c != ']' && c != ',' && c != ':' && c != '{' && c != '[' && c != 65535) ) { //read all content til first space symbol ' ' buf[bufp++] = (char)c; c = getFilech(r); l++; } if(bufp >= bsize && (c != ' ' && c != '\n' && c != '\r' && c != '\t') && (c != '}' && c != ']' && c != ',' && c != ':' && c != '{' && c != '[' && c != 65535) ) { err(c, "end of the token (space symbol or LF or CR or tab) or another token ('}' ']' etc.) but \"..."+(char)c+"\""); return; } String v = new String(buf, 0, l); flushBuf(); if(v.equals("null")) this.curVal = new JsonNull(); else if(v.equals("false")) this.curVal = new JsonBoolean('f'); else if(v.equals("true")) this.curVal = new JsonBoolean('t'); else { double val = ProcessNumber.parseNumber(v); if(Double.isNaN(val)) // NaN (Not a Number) tokens are invalid. err(c, "a number token but found NaN \""+v+"\""); else if(Math.floor(val) == val) this.curVal = new JsonNumber((long) val); else this.curVal = new JsonRealNumber(val); } this.state = JsParserState.READ_STRVALUE; if(ungetch((char) c) == 0) err(c, "available space but OutOfMemory!"); break; } // END of default. } //END of switch }// END COLON state. //BEGIN READ_STRVALUE state. (READ_NON_EMPTY_VALUE) //Проверить контекст перед завершением строки. else if(this.state == JsParserState.READ_STRVALUE){ cur_obj = J_OBJS.top(); JsonArray cur_arr = null; if(c == ',' && J_ROOTS.top() instanceof JsonArray){ cur_arr = J_ARR.top(); cur_arr.getElements().add(this.curVal);// add new item to array this.state = JsParserState.COLON; //awaiting new value this.curVal = null; } else if(c == ',' && J_ROOTS.top() instanceof JsonObject){ cur_obj.getValue().put(props.top(), this.curVal);//add new pair prop : value to the object. props.pop(); this.state = JsParserState.AWAIT_PROPS;//awaiting new property this.curVal = null; } else if(c == ']' && J_ROOTS.top() instanceof JsonArray){ //after processed value follows ']' cur_arr = J_ARR.top(); cur_arr.getElements().add(this.curVal);// add processed value to processed array. J_ARR.pop(); // remove processed array. J_ROOTS.pop();// and update root. this.curVal = null; if(J_ROOTS.top() instanceof JsonArray){ J_ARR.top().getElements().add(cur_arr); //add array as item } else if(J_ROOTS.top() instanceof JsonObject){ J_OBJS.top().getValue().put(props.top(), cur_arr);// add array as property. props.pop(); } this.state = JsParserState.NEXT_VALUE; } else if(c == '}'){ //after processed value follows '}' cur_obj.getValue().put(props.top(), this.curVal); props.pop(); this.curVal = null; if(J_OBJS.size() == 1) // root object finished. this.state = JsParserState.CLOSEROOT; // set final state to exit from cycle. else{ J_OBJS.pop();//remove processed object. J_ROOTS.pop();//and update root. if(J_ROOTS.top() instanceof JsonArray){ J_ARR.top().getElements().add(cur_obj); } else if(J_ROOTS.top() instanceof JsonObject){ J_OBJS.top().getValue().put(props.top(), cur_obj); props.pop(); } this.state = JsParserState.NEXT_VALUE; } } else err(c, "one of ',' '}' ']' "); } //END READ_STRVALUE //BEGIN NEXT_VALUE state else if(this.state == JsParserState.NEXT_VALUE){ cur_obj = J_OBJS.top(); if(c == ',' && J_ROOTS.top() instanceof JsonArray){ this.state = JsParserState.COLON; } else if(c == ',' && J_ROOTS.top() instanceof JsonObject){ this.state = JsParserState.AWAIT_PROPS; } else if(c == ']' && J_ROOTS.top() instanceof JsonArray){ JsonArray cur_arr = J_ARR.top(); J_ARR.pop(); // remove processed array. J_ROOTS.pop();// and update root. if(J_ROOTS.top() instanceof JsonArray){ J_ARR.top().getElements().add(cur_arr); //add array as item } else if(J_ROOTS.top() instanceof JsonObject){ J_OBJS.top().getValue().put(props.top(), cur_arr);// add array as property. props.pop(); } this.state = JsParserState.NEXT_VALUE; } else if(c == '}'){ cur_obj = J_OBJS.top(); if(J_OBJS.size() == 1) // root object finished. this.state = JsParserState.CLOSEROOT; // set final state to exit from cycle. else{ J_OBJS.pop();//remove processed object. J_ROOTS.pop();//and update root. if(J_ROOTS.top() instanceof JsonArray){ J_ARR.top().getElements().add(cur_obj); } else if(J_ROOTS.top() instanceof JsonObject){ J_OBJS.top().getValue().put(props.top(), cur_obj); props.pop(); } this.state = JsParserState.NEXT_VALUE; } } else err(c, "one of ',' '}' ']' "); } // END NEXT_VALUE } //Сигнал об ошибке. private void err(int act, String msg){ state = JsParserState.ERR; System.out.println("Founded illegal symbol \'" + (char)act + "\'" + " at ("+line+":"+col+"). Expected: "+msg); } private int ungetch(char c){ if(bufp >= bsize){ System.out.println("Error ("+line+":"+col+"). ungetch(): too many characters."); return 0; } else{ buf[bufp++] = c; return 1; } } private int getch(InputStreamReader r) throws IOException { if(bufp > 0) return buf[--bufp]; else{ col++; int c = r.read(); if(c == '\n'){ line++; col = 0; } else col += 1; return c; } } //Проверить управляющую последовательность. private int getEscaped(InputStreamReader r) throws IOException { col++; char x = (char)r.read(); switch (x){ case 't':{ col++; return '\t'; } case 'r':{ col++; return '\r'; } case 'n':{ col++; return '\n'; } case 'f':{ col++; return '\f'; } case 'b':{ col++; return '\b'; } case '\'':{ col++; return '\''; } case '\"':{ col++; return '\"'; } case '\\':{ col++; return '\\'; } case 'u':{ col++; int i = 0; char[] hcode = new char[4]; while(i < 4 && ( ((x = (char) r.read()) >= '0' && x <='9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f') )){ hcode[i] = x; i++; } if(i < 4) { err(x, "Unicode token \\uxxxx where x one of [0-9] or [A-Fa-f]"); return 0; } int code = (int)ProcessNumber.parse(new String(hcode),null,'N',16,1, 1); //just parse positive hex number to decimal. return code; } default:{ ungetch(x); return '\\'; } } } //Прочитать символ напрямую private int getFilech(InputStreamReader r) throws IOException { int c = r.read(); if(c == '\n'){ //увелчичиваем счётчик строк. line += 1; col = 0; } else{ col += 1; } return c; }
Что касается вспомогательных методов. Методы getch и ungetch(c) извлекают и сохраняют символ с в буфер. Метод getFilech читает символ напрямую с файла. А метод getEscaped проверяет, была ли прочитана управляющая последовательность символов (\n, \r, \t) и возвращает соответствующий ей символ. Метод err используется для сигнала парсеру об ошибке. Обработкой чисел занят ProcessNumber, описанный здесь.
В iterate заложена основная работа парсера. Прочитав символ (с буфера или с файла если буфер пуст), начинаем с состояния START. Убедившись, что мы прочитали открывающуюся фигурную скобку, сохраняем новый объект в стэке объектов и ждём либо начало нового имени свойства (т.к. имена строки, то ждем первые двойные кавычки), либо закрытую фигурную скобку, которая закрывает объект. Переходим в состояние AWAIT_PROPS_OR_END_OF_OBJ. Если это скобка, то переходим в NEXT_VALUE, где проверяем контекст вложенности. Иначе переходим в AWAIT_PROPS, читая имя свойства, до тех пор пока не встретим двойные кавычки. Прочитанные символы запоминаем в буфер. Если превысили размер — ошибка — завершаем работу. Прочитав двойные кавычки, извлекаем строку из буфера (очищая его), запоминаем новое имя свойства и переходим в READ_PROPNAME. В состоянии READ_PROPNAME проверяем, что имя и значение отделяется двоеточием «:», если нет то ошибка. Проверив, идём в COLON, где определяем, чем является значение — массивом, объектом, строкой, числом, булевым литералом или null. Если это строка, то идём в AWAIT_STRVALUE и читаем строку, аналогично как в AWAIT_PROPS. Прочитав строку, запоминаем её и переходим в READ_STRVALUE, где проверяем контекст вложенности. Аналогично, распарсив число, булев-литерал, null, переходим туда же (в READ_STRVALUE). В READ_STRVALUE при проверке контекста, проверяем следующий символ. Если это запятая, то проверяем стэк текущих вершин J_ROOTS, и добавляем новый элемент либо в массив из J_ARRS, либо в объект из J_OBJS (на вершине). Если это скобка, которая закрывает массив или объект, то извлекаем из соответствующих стэков запись, проверяем J_ROOTS, и добавляем элемент к соответствующему родителю. Как только мы прочитаем последнюю фигурную скобку («}») переходим в состояние CLOSEROOT, в результате следующей проверки перед итерацией цикл завершается. При запятой, в зависимости от контекста, мы идём либо в AWAIT_PROPS (объект), либо в COLON (массив). либо в NEXT_VALUE (при закрытии вложенного объекта/массива).
Итоги
Парсер использует явно 4 стэка, вместо рекурсивных вызовов. Кроме этого, он использует буфер для сохранения строк, а также ссылку на текущий распознанный элемент. Парсер игнорирует пробельные символы. Парсер может обрабатывать некоторые управляющие последовательности символов.
Разумеется, API веб-сервисов возвращают данные в виде массивов. Поскольку данный парсер ожидает в качестве корневого элемента объект, а не массив, то для обхода данной проблемы, необходимо либо обернуть массив в объект, либо добавить новые состояния в парсер.
ссылка на оригинал статьи https://habr.com/ru/post/659287/
Добавить комментарий