Здравствуйте, уважаемые читатели Хабра. В данной статье Вы узнаете, как написать свой 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;
this.c = elements.size();
}
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 веб-сервисов возвращают данные в виде массивов. Поскольку данный парсер ожидает в качестве корневого элемента объект, а не массив, то для обхода данной проблемы, необходимо либо обернуть массив в объект, либо добавить новые состояния в парсер.
Комментарии (17)
superconductor
05.04.2022 23:15+3return "null";
Действительно, почему бы вместо чего-то подобного null'у не вернуть строку "null", ведь давно известно, что из-за этого не случится ничего плохого (нет, https://youtu.be/_c1am8NSx_s)
Это было бы валидно для toString (если toString'ом предлагается этот объект сериализовывать обратно в json, но тогда у всех остальных типов toString вернёт не валидный json), но для getValue это прямо совсем не подходящий вариант.
Поскольку данный парсер ожидает в качестве корневого элемента объект, а не массив
Валидный json может быть не только объектом или массивом, но и любым примитивным типом.
OldNileCrocodile Автор
06.04.2022 09:21Да, тут взято подмножество валидного json. В RFC указано, что помимо объекта, валидным может быть любое значение (массив, строка, число, true, false, null). да и в статье написано, что API-возвращают json-массивы и парсер этого не умеет.
ren-san
06.04.2022 08:39+2Вообще есть много вопросов, начиная с банальных, например, зачем считать в JsonArray count отдельно, хотя можно взять от листа, причём там судя по всему ошибка в setElements, полагаю, надо бы и счётчик обновить.
Заканчивая тем, насколько это вообще будет работать. Если цель была в том, чтобы потренироваться в написании статей — тогда понятно, тренируйтесь дальше. А так сыро, трудночитаемо, и не очень полезно
Almazko
06.04.2022 08:40Тоже писал мини-парсер для своих нужд, но только он получился еще компактнее:
https://github.com/AlmazKo/microjson
Использую его, когда нужна работа с json без биндинга. В больших проектах, конечно лучше брать что-то типа Jackson'aOldNileCrocodile Автор
06.04.2022 12:46+2Ну, во-первых, у вас в методе экземпляра класса Json parseValue() Вы не проверяете литералы true, false, null, а сразу перематываете на нужное число символов, полагаясь на первую букву, хотя это может быть другим словом, на букву t, например, "topa", что сразу не соответствует JSON. Во-вторых, в методе parseObject не учитывается грамматически-правильная последовательность, в том смысле, что после чтения ключа (имени свойства) в строке может идти '}', что нарушает последовательность (ожидался символ
':'
). Т.е. по строке"{ "dummy" }"
вернётся пустой JSON-объект, а должно быть сообщение об ошибке"ожидался ':' а получили сразу '}'"
. (указали в описании репы fail-fast parser). СсылкаAlmazko
06.04.2022 13:39+1Спасибо за фидбек, уже забыл про эти моменты, поправлю! Добавлю в тесты.
Надо соответствовать описанию :)
sYB-Tyumen
06.04.2022 08:58+1Под заголовком "Состояния парсера" было бы хорошо разместить схему (конечный автомат?) состояний парсера. Я, например, заглянул за общим принципом реализации, а не за деталями в коде, поэтому слегка разочарован.
P.S. И вообще, реализация в коде объектов - типов данных просится под кат.
yroman
06.04.2022 16:07Сразу видно - человеку нечем заняться. Мне бы столько свободного времени. Надеюсь, этот велосипед не попадёт ни в какой крупный проект, иначе команде не позавидуешь.
PS: Автотесты-то хоть есть? Или совсем всё плохо?
Felan
06.04.2022 21:37А разве не надо для массивов тоже использовать дженерики в этом случае? А то получается что в конечном итоге, получив из массива объект, нужно будет кастить его тип?
Т.е.
public class JsonArray extends JsonElement<List<JsonElement>>
нужно заменить на что-то типа
public class JsonArray<TT> extends JsonElement<List<JsonElement<TT>>>
Или нет?
OldNileCrocodile Автор
07.04.2022 09:51В Вашем случае всё равно придётся кастить в тип, поскольку JsonObject по строковому ключу извлекает значение в виде JsonElement. Да и при обработке тоже. В стэке ведь могут быть массивы содержащие различные подтипы JsonElement.
Felan
07.04.2022 21:02Че то мне лень вникать, но почему объекты из массива по строковому ключу извлекаются?
А то, что в массиве могут содержаться объекты разных типов это по моему вообще нарушение сути понятия "массив".
Зачем вообще тогда тут городить огород с дженериками если конечному пользователю все равно кастить? Да еще и так опасно.
OldNileCrocodile Автор
08.04.2022 09:23Да, понятие "массив" неккоректно здесь использовать. Правильное понятие - "список". Но само понятие тянется из того же RFC 8259 (везде указано "array").
OldNileCrocodile Автор
08.04.2022 09:25По строкову ключу извлекается значение из объекта. Значение уже может быть массивом, или тем же объектом по типу.
OldNileCrocodile Автор
08.04.2022 09:27А дженерик здесь для getValue() из JsonElement. Но Java не C#. В рантайме у нас всё стирается, и остаётся Object. Поэтому нельзя писать
T a = new T()
. А в C# можно, при условии, что параметр тип T имеет конструктор. Указывается через ограничение whereT : new()
mihmig
Неплохо было бы добавить в статью сравнительные бенчмарки.
dopusteam
Без реализации всего функционала тут сравнивать нечего, имхо. Да и написать json парсер с хорошей производительностью - та ещё задача, тут нет смысла сравнивать даже