JsonNodeTraversingParser.java

/*
 * Copyright 2017-2021 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.jackson.core.tree;

import com.fasterxml.jackson.core.Base64Variant;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonStreamContext;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.base.ParserMinimalBase;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.json.tree.JsonNode;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
import java.util.Map;

import static io.micronaut.core.util.ArrayUtils.EMPTY_CHAR_ARRAY;

/**
 * {@link JsonParser} implementation that iterates over a {@link JsonNode}.
 *
 * @author Jonas Konrad
 * @since 3.1
 */
final class JsonNodeTraversingParser extends ParserMinimalBase {
    private final Deque<Context> contextStack = new ArrayDeque<>();

    private boolean first = true;

    private ObjectCodec codec = null;

    JsonNodeTraversingParser(JsonNode node) {
        Context root;
        if (node.isArray()) {
            root = new ArrayContext(null, node.values().iterator());
        } else if (node.isObject()) {
            root = new ObjectContext(null, node.entries().iterator());
        } else {
            root = new SingleContext(node);
        }
        contextStack.add(root);
    }

    private JsonNode currentNodeOrNull() {
        for (Context context : contextStack) {
            JsonNode node = context.currentNode();
            if (node != null) {
                return node;
            }
        }
        return null;
    }

    @Override
    public JsonToken nextToken() throws IOException {
        if (first) {
            // the context stack starts out positioned on the first token, so we need to intercept the first nextToken call.
            first = false;
            assert !contextStack.isEmpty();
            _currToken = contextStack.peekFirst().currentToken();
            return _currToken;
        }

        while (true) {
            if (contextStack.isEmpty()) {
                return null;
            }
            Context context = contextStack.peekFirst();
            if (context.lastToken) {
                contextStack.removeFirst();
            } else {
                Context childContext = context.next();
                if (childContext != null) {
                    contextStack.addFirst(childContext);
                    _currToken = childContext.currentToken();
                } else {
                    _currToken = context.currentToken();
                }
                return _currToken;
            }
        }
    }

    @Override
    protected void _handleEOF() throws JsonParseException {
        _throwInternal();
    }

    @Override
    public String getCurrentName() throws IOException {
        return contextStack.isEmpty() ? null : contextStack.peekFirst().getCurrentName();
    }

    @Override
    public ObjectCodec getCodec() {
        return codec;
    }

    @Override
    public void setCodec(ObjectCodec oc) {
        this.codec = oc;
    }

    @Override
    public Version version() {
        return Version.unknownVersion();
    }

    @Override
    public void close() throws IOException {
        contextStack.clear();
    }

    @Override
    public boolean isClosed() {
        return contextStack.isEmpty();
    }

    @Override
    public JsonStreamContext getParsingContext() {
        // may be null
        return contextStack.peekFirst();
    }

    @Override
    public JsonLocation getTokenLocation() {
        return JsonLocation.NA;
    }

    @Override
    public JsonLocation getCurrentLocation() {
        return JsonLocation.NA;
    }

    @Override
    public void overrideCurrentName(String name) {
        if (!contextStack.isEmpty()) {
            contextStack.peekFirst().setCurrentName(name);
        }
    }

    @Override
    public String getText() throws IOException {
        if (contextStack.isEmpty()) {
            return null;
        } else {
            return contextStack.peekFirst().getText();
        }
    }

    @Override
    public char[] getTextCharacters() throws IOException {
        String text = getText();
        if (text != null) {
            return text.toCharArray();
        } else {
            return EMPTY_CHAR_ARRAY;
        }
    }

    @Override
    public boolean hasTextCharacters() {
        return false;
    }

    private JsonNode currentNumberNode() throws JsonParseException {
        JsonNode node = currentNodeOrNull();
        if (node != null && node.isNumber()) {
            return node;
        } else {
            throw new JsonParseException(this, "Not a number");
        }
    }

    @Override
    public Number getNumberValue() throws IOException {
        return currentNumberNode().getNumberValue();
    }

    @Override
    public NumberType getNumberType() throws IOException {
        JsonNode currentNode = currentNodeOrNull();
        if (currentNode == null || !currentNode.isNumber()) {
            return null;
        }
        Number value = currentNode.getNumberValue();
        if (value instanceof BigDecimal) {
            return JsonParser.NumberType.BIG_DECIMAL;
        } else if (value instanceof Double) {
            return JsonParser.NumberType.DOUBLE;
        } else if (value instanceof Float) {
            return JsonParser.NumberType.FLOAT;
        } else if (value instanceof Byte || value instanceof Short || value instanceof Integer) {
            return JsonParser.NumberType.INT;
        } else if (value instanceof Long) {
            return JsonParser.NumberType.LONG;
        } else if (value instanceof BigInteger) {
            return JsonParser.NumberType.BIG_INTEGER;
        } else {
            throw new IllegalStateException("Unknown number type " + value.getClass().getName());
        }
    }

    @Override
    public int getIntValue() throws IOException {
        return currentNumberNode().getIntValue();
    }

    @Override
    public long getLongValue() throws IOException {
        return currentNumberNode().getLongValue();
    }

    @Override
    public BigInteger getBigIntegerValue() throws IOException {
        return currentNumberNode().getBigIntegerValue();
    }

    @Override
    public float getFloatValue() throws IOException {
        return currentNumberNode().getFloatValue();
    }

    @Override
    public double getDoubleValue() throws IOException {
        return currentNumberNode().getDoubleValue();
    }

    @Override
    public BigDecimal getDecimalValue() throws IOException {
        return currentNumberNode().getBigDecimalValue();
    }

    @Override
    public int getTextLength() throws IOException {
        String text = getText();
        return text != null ? text.length() : 0;
    }

    @Override
    public int getTextOffset() throws IOException {
        return 0;
    }

    @Override
    public byte[] getBinaryValue(Base64Variant b64variant) throws IOException {
        JsonNode currentNode = currentNodeOrNull();

        if (currentNode != null && currentNode.isNull()) {
            return null;
        }

        String text = getText();
        if (text != null) {
            return b64variant.decode(text);
        }

        return null;
    }

    private static String nodeToText(JsonNode node) {
        if (node.isString()) {
            return node.getStringValue();
        } else if (node.isBoolean()) {
            return Boolean.toString(node.getBooleanValue());
        } else if (node.isNumber()) {
            return node.getNumberValue().toString();
        } else if (node.isNull()) {
            return "null";
        } else if (node.isArray()) {
            return "[";
        } else {
            return "{";
        }
    }

    private static JsonToken asToken(JsonNode node) {
        if (node.isString()) {
            return JsonToken.VALUE_STRING;
        } else if (node.isBoolean()) {
            return node.getBooleanValue() ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE;
        } else if (node.isNumber()) {
            Number numberValue = node.getNumberValue();
            return numberValue instanceof Float || numberValue instanceof Double || numberValue instanceof BigDecimal ?
                JsonToken.VALUE_NUMBER_FLOAT : JsonToken.VALUE_NUMBER_INT;
        } else if (node.isNull()) {
            return JsonToken.VALUE_NULL;
        } else if (node.isArray()) {
            return JsonToken.START_ARRAY;
        } else {
            return JsonToken.START_OBJECT;
        }
    }

    private abstract static class Context extends JsonStreamContext {
        boolean lastToken = false;

        /**
         * Only used to implement {@link JsonStreamContext#getParent()}.
         */
        private final Context parent;

        Context(Context parent) {
            this.parent = parent;
        }

        protected Context createSubContextIfContainer(JsonNode node) {
            if (node.isArray()) {
                return new ArrayContext(this, node.values().iterator());
            } else if (node.isObject()) {
                return new ObjectContext(this, node.entries().iterator());
            } else {
                return null;
            }
        }

        @Override
        public final Context getParent() {
            return parent;
        }

        @Nullable
        abstract Context next();

        @Nullable
        abstract JsonNode currentNode();

        @Override
        public abstract String getCurrentName();

        abstract void setCurrentName(String currentName);

        abstract JsonToken currentToken();

        abstract String getText();
    }

    private static class ArrayContext extends Context {
        final Iterator<JsonNode> iterator;
        /**
         * If {@code null}, we're either at the start or the end array token.
         */
        JsonNode currentNode = null;

        ArrayContext(Context parent, Iterator<JsonNode> iterator) {
            super(parent);
            this._type = TYPE_ARRAY;
            this.iterator = iterator;
        }

        @Override
        @Nullable
        Context next() {
            if (iterator.hasNext()) {
                currentNode = iterator.next();
                return createSubContextIfContainer(currentNode);
            } else {
                currentNode = null;
                lastToken = true;
                return null;
            }
        }

        @Override
        JsonNode currentNode() {
            return currentNode;
        }

        @Override
        public String getCurrentName() {
            return null;
        }

        @Override
        void setCurrentName(String currentName) {
        }

        @Override
        JsonToken currentToken() {
            if (currentNode == null) {
                return lastToken ? JsonToken.END_ARRAY : JsonToken.START_ARRAY;
            } else {
                return asToken(currentNode);
            }
        }

        @Override
        String getText() {
            if (currentNode != null) {
                return nodeToText(currentNode);
            } else {
                return currentToken().asString();
            }
        }
    }

    private static class ObjectContext extends Context {
        final Iterator<Map.Entry<String, JsonNode>> iterator;
        @Nullable
        String currentName = null;
        @Nullable
        JsonNode currentValue = null;

        boolean inFieldName = false;

        ObjectContext(Context parent, Iterator<Map.Entry<String, JsonNode>> iterator) {
            super(parent);
            this._type = TYPE_OBJECT;
            this.iterator = iterator;
        }

        @Nullable
        @Override
        Context next() {
            if (inFieldName) {
                inFieldName = false;
                assert currentValue != null;
                return createSubContextIfContainer(currentValue);
            } else {
                if (iterator.hasNext()) {
                    Map.Entry<String, JsonNode> entry = iterator.next();
                    currentName = entry.getKey();
                    currentValue = entry.getValue();
                    inFieldName = true;
                } else {
                    lastToken = true;
                    currentName = null;
                    currentValue = null;
                }
                return null;
            }
        }

        @Nullable
        @Override
        JsonNode currentNode() {
            return inFieldName ? null : currentValue;
        }

        @Override
        @Nullable
        public String getCurrentName() {
            return currentName;
        }

        @Override
        void setCurrentName(@Nullable String currentName) {
            this.currentName = currentName;
        }

        @Override
        JsonToken currentToken() {
            if (inFieldName) {
                return JsonToken.FIELD_NAME;
            } else if (currentValue != null) {
                return asToken(currentValue);
            } else if (lastToken) {
                return JsonToken.END_OBJECT;
            } else {
                return JsonToken.START_OBJECT;
            }
        }

        @Override
        String getText() {
            if (inFieldName) {
                return currentName;
            } else if (currentValue != null) {
                return nodeToText(currentValue);
            } else {
                return currentToken().asString();
            }
        }
    }

    /**
     * Used as the singular context when the root object we're traversing is a scalar.
     */
    private static class SingleContext extends Context {
        private final JsonNode value;

        SingleContext(JsonNode value) {
            super(null);
            this._type = TYPE_ROOT;
            this.value = value;
            this.lastToken = true;
        }

        @Nullable
        @Override
        Context next() {
            return null;
        }

        @Nullable
        @Override
        JsonNode currentNode() {
            return value;
        }

        @Override
        public String getCurrentName() {
            return null;
        }

        @Override
        void setCurrentName(String currentName) {
        }

        @Override
        JsonToken currentToken() {
            return asToken(value);
        }

        @Override
        String getText() {
            return nodeToText(value);
        }
    }
}