JsonParserImpl.java

package space.sunqian.fs.data.json;

import space.sunqian.annotation.Nonnull;
import space.sunqian.annotation.Nullable;
import space.sunqian.fs.Fs;
import space.sunqian.fs.base.chars.CharsKit;
import space.sunqian.fs.base.number.NumKit;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

enum JsonParserImpl implements JsonParser {
    INST;

    private static final @Nonnull Object NULL = new Object();

    @Override
    public @Nonnull JsonData parse(@Nonnull InputStream input) throws JsonDataParsingException {
        Reader reader = new InputStreamReader(input, CharsKit.defaultCharset());
        return parse(reader);
    }

    @Override
    public @Nonnull JsonData parse(@Nonnull ReadableByteChannel channel) throws JsonDataParsingException {
        // compatible with JDK8
        @SuppressWarnings("CharsetObjectCanBeUsed")
        Reader reader = Channels.newReader(channel, CharsKit.defaultCharset().name());
        return parse(reader);
    }

    @Override
    public @Nonnull JsonData parse(@Nonnull Reader reader) throws JsonDataParsingException {
        try {
            JReader jReader = new JReader(reader);
            Object result = parseJson(jReader, new StringBuilder(), true);
            if (result == null) {
                return JsonData.ofNull();
            }
            if (result instanceof String) {
                return JsonData.ofString((String) result);
            }
            if (result instanceof Boolean) {
                return JsonData.ofBoolean((Boolean) result);
            }
            if (result instanceof Number) {
                return JsonData.ofNumber((Number) result);
            }
            if (result instanceof List<?>) {
                return JsonData.ofList(Fs.as(result));
            }
            return JsonData.ofMap(Fs.as(result));
        } catch (JsonDataParsingException e) {
            throw e;
        } catch (Exception e) {
            throw new JsonDataParsingException(e);
        }
    }

    private Object parseJson(
        @Nonnull JReader reader, @Nonnull StringBuilder strBuilder, boolean toEnd
    ) throws Exception {
        Object result = null;
        int i;
        PARSING:
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (Character.isWhitespace(c)) {
                continue;
            }
            switch (c) {
                case 'n':
                    parseNull(reader);
                    result = NULL;
                    break PARSING;
                case 't':
                    parseTrue(reader);
                    result = true;
                    break PARSING;
                case 'f':
                    parseFalse(reader);
                    result = false;
                    break PARSING;
                case '\"': {
                    parseString(reader, strBuilder);
                    result = strBuilder.toString();
                    strBuilder.setLength(0);
                    break PARSING;
                }
                case '{': {
                    Map<String, Object> objBuilder = new LinkedHashMap<>();
                    parseObject(reader, objBuilder, strBuilder);
                    result = objBuilder;
                    break PARSING;
                }
                case '[': {
                    List<Object> arrBuilder = new ArrayList<>();
                    parseArray(reader, arrBuilder, strBuilder);
                    result = arrBuilder;
                    break PARSING;
                }
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                case '-':
                    strBuilder.append(c);
                    @SuppressWarnings("UnnecessaryLocalVariable")
                    Number number = parseNumber(reader, strBuilder);
                    result = number;
                    strBuilder.setLength(0);
                    break PARSING;
                default:
                    throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), null);
            }
        }
        if (result == null) {
            throw new JsonDataParsingException(reader.nextIndex(), null, null);
        }
        if (result == NULL) {
            return null;
        }
        if (toEnd) {
            skipToEof(reader);
        }
        return result;
    }

    private void parseObject(
        @Nonnull JReader reader,
        @Nonnull Map<@Nonnull String, @Nullable Object> objBuilder,
        @Nonnull StringBuilder strBuilder
    ) throws Exception {
        boolean first = true;
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (Character.isWhitespace(c)) {
                continue;
            }
            switch (c) {
                case '\"':
                    parseString(reader, strBuilder);
                    skipToChar(reader, ':');
                    String key = strBuilder.toString();
                    strBuilder.setLength(0);
                    Object obj = parseJson(reader, strBuilder, false);
                    objBuilder.put(key, obj);
                    first = false;
                    continue;
                case ',':
                    if (!first) {
                        continue;
                    } else {
                        throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), null);
                    }
                case '}':
                    return;
                default:
                    throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), null);
            }
        }
        throw new JsonDataParsingException(reader.nextIndex(), null, "}");
    }

    private void parseArray(
        @Nonnull JReader reader,
        @Nonnull List<@Nullable Object> arrBuilder,
        @Nonnull StringBuilder strBuilder
    ) throws Exception {
        int count = 0;
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (Character.isWhitespace(c)) {
                continue;
            }
            if (c == ',') {
                if (count == 0) {
                    throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), null);
                }
                continue;
            }
            if (c == ']') {
                return;
            }
            // parsing element
            reader.swallow(i);
            Object element = parseJson(reader, strBuilder, false);
            arrBuilder.add(element);
            count++;
        }
        throw new JsonDataParsingException(reader.nextIndex(), null, "]");
    }

    private void parseNull(@Nonnull JReader reader) throws Exception {
        nextChar(reader, 'u');
        nextChar(reader, 'l');
        nextChar(reader, 'l');
    }

    private void parseTrue(@Nonnull JReader reader) throws Exception {
        nextChar(reader, 'r');
        nextChar(reader, 'u');
        nextChar(reader, 'e');
    }

    private void parseFalse(@Nonnull JReader reader) throws Exception {
        nextChar(reader, 'a');
        nextChar(reader, 'l');
        nextChar(reader, 's');
        nextChar(reader, 'e');
    }

    private void parseString(
        @Nonnull JReader reader, @Nonnull StringBuilder builder
    ) throws Exception {
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            switch (c) {
                case '\"':
                    return;
                case '\\':
                    parseEscape(reader, builder);
                    continue;
                default:
                    builder.append(c);
            }
        }
        throw new JsonDataParsingException(reader.nextIndex(), null, "\"");
    }

    private void parseEscape(
        @Nonnull JReader reader, @Nonnull StringBuilder builder
    ) throws Exception {
        int i = reader.nextChar();
        if (i != -1) {
            char c = (char) i;
            switch (c) {
                case '\"':
                case '\\':
                    builder.append(c);
                    return;
                case 'r':
                    builder.append('\r');
                    return;
                case 'n':
                    builder.append('\n');
                    return;
                case 't':
                    builder.append('\t');
                    return;
                case 'b':
                    builder.append('\b');
                    return;
                case 'f':
                    builder.append('\f');
                    return;
                case 'u':
                    parseUnicode(reader, builder);
                    return;
                default:
                    throw new JsonDataParsingException(reader.nextIndex(), String.valueOf(c), null);
            }
        }
        throw new JsonDataParsingException(reader.nextIndex(), null, null);
    }

    private void parseUnicode(
        @Nonnull JReader reader, @Nonnull StringBuilder builder
    ) throws Exception {
        char c1 = nextChar(reader);
        char c2 = nextChar(reader);
        char c3 = nextChar(reader);
        char c4 = nextChar(reader);
        builder.append(CharsKit.unicodeToChar(c1, c2, c3, c4));
    }

    private Number parseNumber(
        @Nonnull JReader reader, @Nonnull StringBuilder strBuilder
    ) throws Exception {
        int startIndex = reader.nextIndex() - 1;
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (
                (c >= '0' && c <= '9')
                    || (c == '.')
                    || (c == 'e')
                    || (c == 'E')
                    || (c == '+')
            ) {
                strBuilder.append(c);
                // continue;
            } else {
                reader.swallow(i);
                break;
            }
        }
        String numberString = strBuilder.toString();
        try {
            return NumKit.toNumber(numberString);
        } catch (Exception e) {
            throw new JsonDataParsingException(startIndex, numberString, null);
        }
    }

    private char nextChar(@Nonnull JReader reader) throws Exception {
        int i = reader.nextChar();
        if (i == -1) {
            throw new JsonDataParsingException(reader.nextIndex(), null, null);
        }
        return (char) i;
    }

    private void nextChar(@Nonnull JReader reader, char shouldBe) throws Exception {
        int i = reader.nextChar();
        if (i == -1) {
            throw new JsonDataParsingException(reader.nextIndex(), null, String.valueOf(shouldBe));
        }
        char c = (char) i;
        if (shouldBe != c) {
            throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), String.valueOf(shouldBe));
        }
    }

    private void skipToEof(@Nonnull JReader reader) throws Exception {
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (!Character.isWhitespace(c)) {
                throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), null);
            }
        }
    }

    @SuppressWarnings("SameParameterValue")
    private void skipToChar(@Nonnull JReader reader, char target) throws Exception {
        int i;
        while ((i = reader.nextChar()) != -1) {
            char c = (char) i;
            if (c == target) {
                return;
            }
            if (!Character.isWhitespace(c)) {
                throw new JsonDataParsingException(reader.nextIndex() - 1, String.valueOf(c), String.valueOf(target));
            }
        }
        throw new JsonDataParsingException(reader.nextIndex(), null, String.valueOf(target));
    }

    private static final class JReader {

        private final @Nonnull Reader reader;
        private int index = 0;
        private int swallowedChar = -1;

        private JReader(@Nonnull Reader reader) {
            this.reader = reader;
        }

        public int nextChar() throws IOException {
            int result;
            if (swallowedChar != -1) {
                result = swallowedChar;
                swallowedChar = -1;
            } else {
                result = reader.read();
            }
            if (result != -1) {
                index++;
            }
            return result;
        }

        public int nextIndex() {
            return index;
        }

        public void swallow(int buf) {
            this.swallowedChar = buf;
            index--;
        }
    }
}