Yet another JSON-парсер

от автора

Здравствуйте, уважаемые читатели Хабра. В данной статье Вы узнаете, как написать свой 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/