JsonNodeTreeCodec.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.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import io.micronaut.core.annotation.Experimental;
import io.micronaut.json.JsonStreamConfig;
import io.micronaut.json.tree.JsonNode;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Codec for transforming {@link JsonNode} from and to json streams.
 *
 * @author Jonas Konrad
 * @since 3.1
 */
@Experimental
public final class JsonNodeTreeCodec {
    private static final JsonNodeTreeCodec INSTANCE = new JsonNodeTreeCodec(JsonStreamConfig.DEFAULT);

    private final JsonStreamConfig config;

    private JsonNodeTreeCodec(JsonStreamConfig config) {
        this.config = config;
    }

    /**
     * @return The default instance, using {@link JsonStreamConfig#DEFAULT}.
     */
    public static JsonNodeTreeCodec getInstance() {
        return INSTANCE;
    }

    /**
     * @param config The stream config to use.
     * @return A new codec that will use the given stream config.
     */
    public JsonNodeTreeCodec withConfig(JsonStreamConfig config) {
        return new JsonNodeTreeCodec(config);
    }

    /**
     * Read a json node from a stream.
     *
     * @param p The stream to parse.
     * @return The parsed json node.
     * @throws IOException IOException
     */
    public JsonNode readTree(JsonParser p) throws IOException {
        switch (p.hasCurrentToken() ? p.currentToken() : p.nextToken()) {
            case START_OBJECT:
                var map = new LinkedHashMap<String, JsonNode>();
                while (p.nextToken() != JsonToken.END_OBJECT) {
                    String name = p.currentName();
                    p.nextToken();
                    map.put(name, readTree(p));
                }
                return JsonNode.createObjectNode(map);
            case START_ARRAY:
                var list = new ArrayList<JsonNode>();
                while (p.nextToken() != JsonToken.END_ARRAY) {
                    list.add(readTree(p));
                }
                return JsonNode.createArrayNode(list);
            case VALUE_STRING:
                return JsonNode.createStringNode(p.getText());
            case VALUE_NUMBER_INT:
                if (config.useBigIntegerForInts()) {
                    return JsonNode.createNumberNode(p.getBigIntegerValue());
                } else {
                    // technically, we could get an unsupported number value here.
                    return JsonNode.createNumberNodeImpl(p.getNumberValue());
                }
            case VALUE_NUMBER_FLOAT:
                if (config.useBigDecimalForFloats()) {
                    // NaN and Inf can't be represented by BigDecimal. Note: isNaN returns true for Inf, too.
                    if (p.isNaN()) {
                        return JsonNode.createNumberNode(p.getFloatValue());
                    } else {
                        return JsonNode.createNumberNode(p.getDecimalValue());
                    }
                } else {
                    // technically, we could get an unsupported number value here.
                    return JsonNode.createNumberNodeImpl(p.getNumberValue());
                }
            case VALUE_TRUE:
                return JsonNode.createBooleanNode(true);
            case VALUE_FALSE:
                return JsonNode.createBooleanNode(false);
            case VALUE_NULL:
                return JsonNode.nullNode();
            default:
                throw new UnsupportedOperationException("Unsupported token: " + p.currentToken());
        }
    }

    /**
     * Write a json node to a json stream.
     *
     * @param generator The output json stream.
     * @param tree The node to write.
     * @throws IOException IOException
     */
    public void writeTree(JsonGenerator generator, JsonNode tree) throws IOException {
        if (tree.isObject()) {
            generator.writeStartObject();
            for (Map.Entry<String, JsonNode> entry : tree.entries()) {
                generator.writeFieldName(entry.getKey());
                writeTree(generator, entry.getValue());
            }
            generator.writeEndObject();
        } else if (tree.isArray()) {
            generator.writeStartArray();
            for (JsonNode value : tree.values()) {
                writeTree(generator, value);
            }
            generator.writeEndArray();
        } else if (tree.isBoolean()) {
            generator.writeBoolean(tree.getBooleanValue());
        } else if (tree.isNull()) {
            generator.writeNull();
        } else if (tree.isNumber()) {
            Number value = tree.getNumberValue();
            // integer, long, double are the most common. Check those first.
            if (value instanceof Integer) {
                generator.writeNumber(value.intValue());
            } else if (value instanceof Long) {
                generator.writeNumber(value.longValue());
            } else if (value instanceof Double) {
                generator.writeNumber(value.doubleValue());
            } else if (value instanceof Float) {
                generator.writeNumber(value.floatValue());
            } else if (value instanceof BigDecimal decimal) {
                generator.writeNumber(decimal);
            } else if (value instanceof Byte || value instanceof Short) {
                generator.writeNumber(value.shortValue());
            } else if (value instanceof BigInteger integer) {
                generator.writeNumber(integer);
            } else {
                throw new IllegalStateException("Unknown number type " + value.getClass().getName());
            }
        } else if (tree.isString()) {
            generator.writeString(tree.getStringValue());
        } else {
            throw new AssertionError();
        }
    }

    /**
     * Create a new parser that traverses over the given json node.
     *
     * @param node The json node to traverse over.
     * @return The parser that will visit the json node.
     */
    public JsonParser treeAsTokens(JsonNode node) {
        return new JsonNodeTraversingParser(node);
    }

    /**
     * Create a {@link JsonGenerator} that will return a {@link JsonNode} when completed.
     *
     * @return The generator.
     */
    public TreeGenerator createTreeGenerator() {
        return new TreeGenerator();
    }
}