NodeCalc.java

/**
 * Copyright (c) 2018, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.timeseries.ast;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.powsybl.commons.json.JsonUtil;
import com.powsybl.timeseries.TimeSeriesException;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Deque;
import java.util.Objects;

/**
 * A NodeCalc is an element of the timeseries computation tree. These
 * computation trees are typically the results of running user scripts, for
 * example written in groovy. They can be serialized to json or traversed by
 * visitors. Traversing them with visitor allows to compute various results,
 * such as evaluating the tree or find the names of the timeseries used in the
 * tree.
 *
 * <p>The writeJson method is used to serialize the tree to json.
 *
 * <p>The accept, acceptIterate and acceptHandle methods together with the
 * {@link NodeCalcVisitor} interface form the
 * hybrid recursive/iterative visitor pattern. This visitor pattern
 * uses recursion on the children up to a stack depth limit because
 * performance is almost 5 times better when using recursion compared
 * to the iterative algorithm using stacks, but excessive depths cause
 * StackOverflowErrors.
 *
 * <p>The accept method are the main entrypoint of the hybrid visit and
 * implements the recursive visit as well as performing the switch from
 * the recursive to iterative behavior.
 *
 * <p>The acceptIterate and acceptHandle methods are used by {@link NodeCalcVisitors}
 * during the iterative traversal of the tree. The
 * acceptIterate method push children nodes the be traversed in the
 * stack.  The acceptHandle method extract the already calculated
 * children results from the stack and use them to compute and return
 * the result for this node.
 *
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public interface NodeCalc {

    <R, A> R accept(NodeCalcVisitor<R, A> visitor, A arg, int depth);

    <R, A> void acceptIterate(NodeCalcVisitor<R, A> visitor, A arg, Deque<Object> nodesStack);

    <R, A> R acceptHandle(NodeCalcVisitor<R, A> visitor, A arg, Deque<Object> resultsStack);

    void writeJson(JsonGenerator generator) throws IOException;

    static void writeJson(NodeCalc node, JsonGenerator generator) {
        try {
            generator.writeStartObject();
            node.writeJson(generator);
            generator.writeEndObject();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    static String toJson(NodeCalc node) {
        Objects.requireNonNull(node);
        return JsonUtil.toJson(generator -> writeJson(node, generator));
    }

    static NodeCalc parseJson(String json) {
        return JsonUtil.parseJson(json, NodeCalc::parseJson);
    }

    static NodeCalc parseJson(JsonParser parser) {
        Objects.requireNonNull(parser);
        try {
            NodeCalc nodeCalc = null;
            JsonToken token;
            boolean continueLoop = true;
            while (continueLoop && (token = parser.nextToken()) != null) {
                switch (token) {
                    case START_OBJECT -> {
                        // Do nothing
                    }
                    case END_OBJECT -> continueLoop = false;
                    default -> nodeCalc = parseJson(parser, token);
                }
            }
            return nodeCalc;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    static TimeSeriesException createUnexpectedToken(JsonToken token) {
        return new TimeSeriesException("Unexpected JSON token: " + token);
    }

    static NodeCalc parseJson(JsonParser parser, JsonToken token) throws IOException {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(token);
        if (token == JsonToken.FIELD_NAME) {
            String fieldName = parser.currentName();
            switch (fieldName) {
                case IntegerNodeCalc.NAME -> {
                    return IntegerNodeCalc.parseJson(parser);
                }
                case FloatNodeCalc.NAME -> {
                    return FloatNodeCalc.parseJson(parser);
                }
                case DoubleNodeCalc.NAME -> {
                    return DoubleNodeCalc.parseJson(parser);
                }
                case BigDecimalNodeCalc.NAME -> {
                    return BigDecimalNodeCalc.parseJson(parser);
                }
                case BinaryOperation.NAME -> {
                    return BinaryOperation.parseJson(parser);
                }
                case UnaryOperation.NAME -> {
                    return UnaryOperation.parseJson(parser);
                }
                case MinNodeCalc.NAME -> {
                    return MinNodeCalc.parseJson(parser);
                }
                case MaxNodeCalc.NAME -> {
                    return MaxNodeCalc.parseJson(parser);
                }
                case TimeSeriesNameNodeCalc.NAME -> {
                    return TimeSeriesNameNodeCalc.parseJson(parser);
                }
                case TimeNodeCalc.NAME -> {
                    return TimeNodeCalc.parseJson(parser);
                }
                case BinaryMinCalc.NAME -> {
                    return BinaryMinCalc.parseJson(parser);
                }
                case BinaryMaxCalc.NAME -> {
                    return BinaryMaxCalc.parseJson(parser);
                }
                default -> {
                    // Do nothing
                }
            }
        }
        throw createUnexpectedToken(token);
    }
}