TreeTraversingParser.java

package tools.jackson.databind.node;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;

import tools.jackson.core.*;
import tools.jackson.core.base.ParserMinimalBase;
import tools.jackson.core.exc.InputCoercionException;
import tools.jackson.core.util.JacksonFeatureSet;
import tools.jackson.databind.JsonNode;

/**
 * Facade over {@link JsonNode} that implements {@link JsonParser} to allow
 * accessing contents of JSON tree in alternate form (stream of tokens).
 * Useful when a streaming source is expected by code, such as data binding
 * functionality.
 */
public class TreeTraversingParser
    extends ParserMinimalBase
{
    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    /**
     * @since 3.0
     */
    protected final JsonNode _source;

    /**
     * Traversal context within tree
     */
    protected NodeCursor _nodeCursor;

    /*
    /**********************************************************************
    /* State
    /**********************************************************************
     */

    /**
     * Flag that indicates whether parser is closed or not. Gets
     * set when parser is either closed by explicit call
     * ({@link #close}) or when end-of-input is reached.
     */
    protected boolean _closed;

    /*
    /**********************************************************************
    /* Life-cycle
    /**********************************************************************
     */

    public TreeTraversingParser(JsonNode n) { this(n, ObjectReadContext.empty()); }

    public TreeTraversingParser(JsonNode n, ObjectReadContext readContext)
    {
        super(readContext);
        _source = n;
        _nodeCursor = new NodeCursor.RootCursor(n, null);
    }

    @Override
    public Version version() {
        return tools.jackson.databind.cfg.PackageVersion.VERSION;
    }

    @Override
    public JacksonFeatureSet<StreamReadCapability> streamReadCapabilities() {
        // Defaults are fine
        return DEFAULT_READ_CAPABILITIES;
    }

    @Override
    public JsonNode streamReadInputSource() {
        return _source;
    }

    // Default from base class should be fine:
    //public StreamReadConstraints streamReadConstraints() {

    /*
    /**********************************************************************
    /* Closing, related
    /**********************************************************************
     */

    @Override
    public void close()
    {
        if (!_closed) {
            _closed = true;
            _nodeCursor = null;
            _updateTokenToNull();
        }
    }

    @Override
    protected void _closeInput() throws IOException { }

    @Override
    protected void _releaseBuffers() { }

    /*
    /**********************************************************************
    /* Public API, traversal
    /**********************************************************************
     */

    @Override
    public JsonToken nextToken()
    {
        _nullSafeUpdateToken(_nodeCursor.nextToken());
        if (_currToken == null) {
            _closed = true; // if not already set
            return null;
        }
        switch (_currToken) {
        case START_OBJECT:
            _nodeCursor = _nodeCursor.startObject();
            break;
        case START_ARRAY:
            _nodeCursor = _nodeCursor.startArray();
            break;
        case END_OBJECT:
        case END_ARRAY:
            _nodeCursor = _nodeCursor.getParent();
        default:
        }
        return _currToken;
    }

    // default works well here:
    //public JsonToken nextValue()

    @Override
    public JsonParser skipChildren()
    {
        if (_currToken == JsonToken.START_OBJECT) {
            _nodeCursor = _nodeCursor.getParent();
            _updateToken(JsonToken.END_OBJECT);
        } else if (_currToken == JsonToken.START_ARRAY) {
            _nodeCursor = _nodeCursor.getParent();
            _updateToken(JsonToken.END_ARRAY);
        }
        return this;
    }

    @Override
    public boolean isClosed() {
        return _closed;
    }

    /*
    /**********************************************************************
    /* Public API, token accessors
    /**********************************************************************
     */

    @Override public String currentName() {
        NodeCursor crsr = _nodeCursor;
        if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
            crsr = crsr.getParent();
        }
        return (crsr == null) ? null : crsr.currentName();
    }

    @Override
    public TokenStreamContext streamReadContext() {
        return _nodeCursor;
    }

    @Override public void assignCurrentValue(Object v) { _nodeCursor.assignCurrentValue(v); }
    @Override public Object currentValue() { return _nodeCursor.currentValue(); }

    @Override
    public TokenStreamLocation currentTokenLocation() {
        return TokenStreamLocation.NA;
    }

    @Override
    public TokenStreamLocation currentLocation() {
        return TokenStreamLocation.NA;
    }

    /*
    /**********************************************************************
    /* Public API, access to textual content
    /**********************************************************************
     */

    @Override
    public String getString()
    {
        if (_currToken == null) {
            return null;
        }
        // need to separate handling a bit...
        switch (_currToken) {
        case PROPERTY_NAME:
            return _nodeCursor.currentName();
        case VALUE_STRING:
            return currentNode().stringValue();
        case VALUE_NUMBER_INT:
        case VALUE_NUMBER_FLOAT:
            return String.valueOf(currentNode().numberValue());
        case VALUE_EMBEDDED_OBJECT:
            JsonNode n = currentNode();
            if (n != null && n.isBinary()) {
                // this will convert it to base64
                return n.asString();
            }
        default:
            return _currToken.asString();
        }
    }

    @Override
    public char[] getStringCharacters() {
        return getString().toCharArray();
    }

    @Override
    public int getStringLength() {
        return getString().length();
    }

    @Override
    public int getStringOffset() {
        return 0;
    }

    @Override
    public boolean hasStringCharacters() {
        // generally we do not have efficient access as char[], hence:
        return false;
    }

    /*
    /**********************************************************************
    /* Public API, typed non-text access
    /**********************************************************************
     */

    //public byte getByteValue();

    @Override
    public NumberType getNumberType() {
        // NOTE: do not call "currentNumericNode()" as that would throw exception
        // on non-numeric node
        JsonNode n = currentNode();
        if (n instanceof NumericNode) {
            return n.numberType();
        }
        return null;
    }

    @Override
    public NumberTypeFP getNumberTypeFP() {
        NumberType nt = getNumberType();
        if (nt == NumberType.BIG_DECIMAL) {
            return NumberTypeFP.BIG_DECIMAL;
        }
        if (nt == NumberType.DOUBLE) {
            return NumberTypeFP.DOUBLE64;
        }
        if (nt == NumberType.FLOAT) {
            return NumberTypeFP.FLOAT32;
        }
        return NumberTypeFP.UNKNOWN;
    }

    @Override
    public BigInteger getBigIntegerValue() throws InputCoercionException {
        return currentNumericNode(NR_BIGINT).bigIntegerValue();
    }

    @Override
    public BigDecimal getDecimalValue() throws InputCoercionException {
        return currentNumericNode(NR_BIGDECIMAL).decimalValue();
    }

    @Override
    public double getDoubleValue() throws InputCoercionException {
        return currentNumericNode(NR_DOUBLE).doubleValue();
    }

    @Override
    public float getFloatValue() throws InputCoercionException {
        return (float) currentNumericNode(NR_FLOAT).doubleValue();
    }

    @Override
    public short getShortValue() throws InputCoercionException {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (!node.canConvertToShort()) {
            String desc = _longIntegerDesc(node.asString());
            if (!node.canConvertToExactIntegral()) {
                throw _constructInputCoercion(String.format(
"Numeric value (%s) of `%s` has fractional part; cannot convert to `short`",
                        desc, node.getClass().getSimpleName()),
                    node.asToken(), Integer.TYPE);
            }
            // otherwise assume range overflow
            _reportOverflowShort(desc, currentToken());
        }
        return node.shortValue();
    }

    @Override
    public int getIntValue() throws InputCoercionException {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (!node.canConvertToInt()) {
            // [databind#5309] Misleading exception message for DoubleNode to `int` value conversion
            if (!node.canConvertToExactIntegral()) {
                throw _constructInputCoercion(String.format(
"Numeric value (%s) of `%s` has fractional part; cannot convert to `int`",
                            _longIntegerDesc(node.asString()),
                            node.getClass().getSimpleName()
                        ),
                        node.asToken(), Integer.TYPE);
            }
            // otherwise assume range overflow
            _reportOverflowInt();
        }
        return node.intValue();
    }

    @Override
    public int getValueAsInt()
    {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (node.inIntRange()) {
            return node._asIntValueUnchecked();
        }
        // !!! TODO: better defaulting/coercion?
        return getIntValue();
    }

    @Override
    public int getValueAsInt(int defaultValue)
    {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (node.inIntRange()) {
            return node._asIntValueUnchecked();
        }
        // !!! TODO: better defaulting/coercion?
        return defaultValue;
    }

    @Override
    public long getLongValue() throws InputCoercionException {
        final NumericNode node = (NumericNode) currentNumericNode(NR_LONG);
        if (!node.canConvertToLong()) {
            // [databind#5309] Misleading exception message for DoubleNode to `long` value conversion
            if (!node.canConvertToExactIntegral()) {
                throw _constructInputCoercion(String.format(
"Numeric value (%s) of `%s` has fractional part; cannot convert to `long`",
                            _longIntegerDesc(node.asString()),
                            node.getClass().getSimpleName()
                        ),
                        node.asToken(), Long.TYPE);
            }
            // otherwise assume range overflow
            _reportOverflowLong();
        }
        return node.longValue();
    }

    @Override
    public long getValueAsLong()
    {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (node.inLongRange()) {
            return node._asLongValueUnchecked();
        }
        // !!! TODO: better defaulting/coercion?
        return getLongValue();
    }

    @Override
    public long getValueAsLong(long defaultValue)
    {
        final NumericNode node = (NumericNode) currentNumericNode(NR_INT);
        if (node.inLongRange()) {
            return node._asLongValueUnchecked();
        }
        // !!! TODO: better defaulting/coercion?
        return defaultValue;
    }
    
    @Override
    public Number getNumberValue() throws InputCoercionException {
        return currentNumericNode(-1).numberValue();
    }

    @Override
    public Object getEmbeddedObject()
    {
        if (!_closed) {
            JsonNode n = currentNode();
            if (n != null) {
                if (n.isPojo()) {
                    return ((POJONode) n).getPojo();
                }
                if (n.isBinary()) {
                    return ((BinaryNode) n).binaryValue();
                }
            }
        }
        return null;
    }

    @Override
    public boolean isNaN() {
        if (!_closed) {
            JsonNode n = currentNode();
            if (n instanceof NumericNode numericNode) {
                return numericNode.isNaN();
            }
        }
        return false;
    }

    /*
    /**********************************************************************
    /* Public API, typed binary (base64) access
    /**********************************************************************
     */

    @Override
    public byte[] getBinaryValue(Base64Variant b64variant)
        throws JacksonException
    {
        // Multiple possibilities...
        JsonNode n = currentNode();
        if (n != null) {
            // [databind#2096]: although `binaryValue()` works for real binary node
            // and embedded "POJO" node, coercion from `StringNode` may require variant, so:
            if (n instanceof StringNode stringNode) {
                return stringNode.getBinaryValue(b64variant);
            }
            return n.binaryValue();
        }
        // otherwise return null to mark we have no binary content
        return null;
    }


    @Override
    public int readBinaryValue(Base64Variant b64variant, OutputStream out)
        throws JacksonException
    {
        byte[] data = getBinaryValue(b64variant);
        if (data != null) {
            try {
                out.write(data, 0, data.length);
            } catch (IOException e) {
                throw _wrapIOFailure(e);
            }
            return data.length;
        }
        return 0;
    }

    /*
    /**********************************************************************
    /* Internal methods
    /**********************************************************************
     */

    protected JsonNode currentNode() {
        if (_closed || _nodeCursor == null) {
            return null;
        }
        return _nodeCursor.currentNode();
    }

    protected JsonNode currentNumericNode(int targetNumType)
    {
        JsonNode n = currentNode();
        if (n == null || !n.isNumber()) {
            JsonToken t = (n == null) ? null : n.asToken();
            throw _constructNotNumericType(t, -1);
        }
        return n;
    }

    @Override
    protected void _handleEOF() {
        _throwInternal(); // should never get called
    }
}